From 800f3108e51e7fc9f7dd48a1ba278bc567eb3758 Mon Sep 17 00:00:00 2001 From: Robert E Date: Thu, 22 Aug 2024 21:57:38 +1000 Subject: [PATCH 1/8] Convert WindowsX509CertificateStore.cs from static to class --- .../IWindowsX509CertificateStore.cs | 38 +++++++++++++++ .../NoOpWindowsX509CertificateStore.cs | 46 +++++++++++++++++++ .../Certificates/PrivateKeyAccessRule.cs | 21 +-------- .../WindowsX509CertificateStore.cs | 32 +++++++++---- .../WindowsX509CertificateStoreFixture.cs | 20 ++++---- .../Calamari/Commands/DeployPackageCommand.cs | 9 ++-- .../Commands/ImportCertificateCommand.cs | 25 +++++----- .../IisWebSiteAfterPostDeployFeature.cs | 21 ++++++--- .../Features/IisWebSiteBeforeDeployFeature.cs | 25 +++++----- source/Calamari/Program.cs | 8 ++++ 10 files changed, 173 insertions(+), 72 deletions(-) create mode 100644 source/Calamari.Shared/Integration/Certificates/IWindowsX509CertificateStore.cs create mode 100644 source/Calamari.Shared/Integration/Certificates/NoOpWindowsX509CertificateStore.cs diff --git a/source/Calamari.Shared/Integration/Certificates/IWindowsX509CertificateStore.cs b/source/Calamari.Shared/Integration/Certificates/IWindowsX509CertificateStore.cs new file mode 100644 index 000000000..d1988cc0e --- /dev/null +++ b/source/Calamari.Shared/Integration/Certificates/IWindowsX509CertificateStore.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace Calamari.Integration.Certificates +{ + + public static class WindowsX509CertificateConstants + { + public static readonly string RootAuthorityStoreName = "Root"; + } + + public interface IWindowsX509CertificateStore + { + string? FindCertificateStore(string thumbprint, StoreLocation storeLocation); + + void ImportCertificateToStore(byte[] pfxBytes, + string password, + StoreLocation storeLocation, + string storeName, + bool privateKeyExportable); + + void AddPrivateKeyAccessRules(string thumbprint, + StoreLocation storeLocation, + ICollection privateKeyAccessRules); + + void AddPrivateKeyAccessRules(string thumbprint, + StoreLocation storeLocation, + string storeName, + ICollection privateKeyAccessRules); + + + void ImportCertificateToStore(byte[] pfxBytes, + string password, + string userName, + string storeName, + bool privateKeyExportable); + } +} \ No newline at end of file diff --git a/source/Calamari.Shared/Integration/Certificates/NoOpWindowsX509CertificateStore.cs b/source/Calamari.Shared/Integration/Certificates/NoOpWindowsX509CertificateStore.cs new file mode 100644 index 000000000..5a2023955 --- /dev/null +++ b/source/Calamari.Shared/Integration/Certificates/NoOpWindowsX509CertificateStore.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace Calamari.Integration.Certificates +{ + /// + /// Stand in replacement for IWindowsX509CertificateStore that will be registered for non Windows machines. + /// This should never end up being called. If it is, something has gone wrong somewhere else + /// + public class NoOpWindowsX509CertificateStore: IWindowsX509CertificateStore + { + public string? FindCertificateStore(string thumbprint, StoreLocation storeLocation) + { + throw new System.NotImplementedException(); + } + + public void ImportCertificateToStore(byte[] pfxBytes, + string password, + StoreLocation storeLocation, + string storeName, + bool privateKeyExportable) + { + throw new System.NotImplementedException(); + } + + public void AddPrivateKeyAccessRules(string thumbprint, StoreLocation storeLocation, ICollection privateKeyAccessRules) + { + throw new System.NotImplementedException(); + } + + public void AddPrivateKeyAccessRules(string thumbprint, StoreLocation storeLocation, string storeName, ICollection privateKeyAccessRules) + { + throw new System.NotImplementedException(); + } + + public void ImportCertificateToStore(byte[] pfxBytes, + string password, + string userName, + string storeName, + bool privateKeyExportable) + { + throw new System.NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/source/Calamari.Shared/Integration/Certificates/PrivateKeyAccessRule.cs b/source/Calamari.Shared/Integration/Certificates/PrivateKeyAccessRule.cs index d4bfb31f8..677ad6336 100644 --- a/source/Calamari.Shared/Integration/Certificates/PrivateKeyAccessRule.cs +++ b/source/Calamari.Shared/Integration/Certificates/PrivateKeyAccessRule.cs @@ -1,7 +1,5 @@ -#if WINDOWS_CERTIFICATE_STORE_SUPPORT -using System; +using System; using System.Collections.Generic; -using System.Security.AccessControl; using System.Security.Principal; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -30,22 +28,6 @@ public static ICollection FromJson(string json) return JsonConvert.DeserializeObject>(json, JsonSerializerSettings); } - internal CryptoKeyAccessRule ToCryptoKeyAccessRule() - { - switch (Access) - { - case PrivateKeyAccess.ReadOnly: - return new CryptoKeyAccessRule(Identity, CryptoKeyRights.GenericRead, AccessControlType.Allow); - - case PrivateKeyAccess.FullControl: - // We use 'GenericAll' here rather than 'FullControl' as 'FullControl' doesn't correctly set the access for CNG keys - return new CryptoKeyAccessRule(Identity, CryptoKeyRights.GenericAll, AccessControlType.Allow); - - default: - throw new ArgumentOutOfRangeException(nameof(Access)); - } - } - private static JsonSerializerSettings JsonSerializerSettings => new JsonSerializerSettings { Converters = new List @@ -56,4 +38,3 @@ internal CryptoKeyAccessRule ToCryptoKeyAccessRule() } } -#endif \ No newline at end of file diff --git a/source/Calamari.Shared/Integration/Certificates/WindowsX509CertificateStore.cs b/source/Calamari.Shared/Integration/Certificates/WindowsX509CertificateStore.cs index b7a6851a2..d513ce16f 100644 --- a/source/Calamari.Shared/Integration/Certificates/WindowsX509CertificateStore.cs +++ b/source/Calamari.Shared/Integration/Certificates/WindowsX509CertificateStore.cs @@ -18,7 +18,7 @@ namespace Calamari.Integration.Certificates { - public class WindowsX509CertificateStore + public class WindowsX509CertificateStore: IWindowsX509CertificateStore { public static readonly ISemaphoreFactory Semaphores = SemaphoreFactory.Get(); public static readonly string SemaphoreName = nameof(WindowsX509CertificateStore); @@ -31,7 +31,7 @@ private static IDisposable AcquireSemaphore() return Semaphores.Acquire(SemaphoreName, "Another process is working with the certificate store, please wait..."); } - public static string? FindCertificateStore(string thumbprint, StoreLocation storeLocation) + public string? FindCertificateStore(string thumbprint, StoreLocation storeLocation) { foreach (var storeName in GetStoreNames(storeLocation)) { @@ -50,7 +50,7 @@ private static IDisposable AcquireSemaphore() return null; } - public static void ImportCertificateToStore(byte[] pfxBytes, string password, StoreLocation storeLocation, + public void ImportCertificateToStore(byte[] pfxBytes, string password, StoreLocation storeLocation, string storeName, bool privateKeyExportable) { using (AcquireSemaphore()) @@ -80,7 +80,7 @@ public static void ImportCertificateToStore(byte[] pfxBytes, string password, St /// /// Import a certificate into a specific user's store /// - public static void ImportCertificateToStore(byte[] pfxBytes, string password, string userName, + public void ImportCertificateToStore(byte[] pfxBytes, string password, string userName, string storeName, bool privateKeyExportable) { using (AcquireSemaphore()) @@ -103,7 +103,7 @@ public static void ImportCertificateToStore(byte[] pfxBytes, string password, st } } - public static void AddPrivateKeyAccessRules(string thumbprint, + public void AddPrivateKeyAccessRules(string thumbprint, StoreLocation storeLocation, ICollection privateKeyAccessRules) { @@ -111,7 +111,7 @@ public static void AddPrivateKeyAccessRules(string thumbprint, AddPrivateKeyAccessRules(thumbprint, storeLocation, storeName, privateKeyAccessRules); } - public static void AddPrivateKeyAccessRules(string thumbprint, StoreLocation storeLocation, string storeName, + public void AddPrivateKeyAccessRules(string thumbprint, StoreLocation storeLocation, string storeName, ICollection privateKeyAccessRules) { using (AcquireSemaphore()) @@ -432,6 +432,22 @@ static void AddPrivateKeyAccessRules(ICollection accessRul throw new Exception("Could not set security on private-key", ex); } } + + static CryptoKeyAccessRule ToCryptoKeyAccessRule(PrivateKeyAccessRule rule) + { + switch (rule.Access) + { + case PrivateKeyAccess.ReadOnly: + return new CryptoKeyAccessRule(rule.Identity, CryptoKeyRights.GenericRead, AccessControlType.Allow); + + case PrivateKeyAccess.FullControl: + // We use 'GenericAll' here rather than 'FullControl' as 'FullControl' doesn't correctly set the access for CNG keys + return new CryptoKeyAccessRule(rule.Identity, CryptoKeyRights.GenericAll, AccessControlType.Allow); + + default: + throw new ArgumentOutOfRangeException(nameof(rule.Access)); + } + } static void SetCngPrivateKeySecurity(SafeCertContextHandle certificate, ICollection accessRules) { @@ -439,7 +455,7 @@ static void SetCngPrivateKeySecurity(SafeCertContextHandle certificate, ICollect { var security = GetCngPrivateKeySecurity(certificate); - foreach (var cryptoKeyAccessRule in accessRules.Select(r => r.ToCryptoKeyAccessRule())) + foreach (var cryptoKeyAccessRule in accessRules.Select(r => ToCryptoKeyAccessRule(r))) { security.AddAccessRule(cryptoKeyAccessRule); } @@ -468,7 +484,7 @@ static void SetCspPrivateKeySecurity(SafeCertContextHandle certificate, ICollect { var security = GetCspPrivateKeySecurity(certificate); - foreach (var cryptoKeyAccessRule in accessRules.Select(r => r.ToCryptoKeyAccessRule())) + foreach (var cryptoKeyAccessRule in accessRules.Select(r => ToCryptoKeyAccessRule(r))) { security.AddAccessRule(cryptoKeyAccessRule); } diff --git a/source/Calamari.Tests/Fixtures/Certificates/WindowsX509CertificateStoreFixture.cs b/source/Calamari.Tests/Fixtures/Certificates/WindowsX509CertificateStoreFixture.cs index 8938b42cf..89292ded4 100644 --- a/source/Calamari.Tests/Fixtures/Certificates/WindowsX509CertificateStoreFixture.cs +++ b/source/Calamari.Tests/Fixtures/Certificates/WindowsX509CertificateStoreFixture.cs @@ -33,7 +33,7 @@ public void CanImportCertificate(string sampleCertificateId, StoreLocation store sampleCertificate.EnsureCertificateNotInStore(storeName, storeLocation); - WindowsX509CertificateStore.ImportCertificateToStore(Convert.FromBase64String(sampleCertificate.Base64Bytes()), sampleCertificate.Password, + new WindowsX509CertificateStore().ImportCertificateToStore(Convert.FromBase64String(sampleCertificate.Base64Bytes()), sampleCertificate.Password, storeLocation, storeName, sampleCertificate.HasPrivateKey); sampleCertificate.AssertCertificateIsInStore(storeName, storeLocation); @@ -64,7 +64,7 @@ public void SafeForConcurrentOperations() var exceptions = new BlockingCollection(); void Log(string message) => Console.WriteLine($"{sw.Elapsed} {Thread.CurrentThread.Name}: {message}"); - WindowsX509CertificateStore.ImportCertificateToStore( + new WindowsX509CertificateStore().ImportCertificateToStore( Convert.FromBase64String(sampleCertificate.Base64Bytes()), sampleCertificate.Password, StoreLocation.LocalMachine, "My", sampleCertificate.HasPrivateKey); @@ -97,14 +97,14 @@ Thread[] CreateThreads(int number, string name, Action action) => Enumerable.Ran var threads = CreateThreads(numThreads, "ImportCertificateToStore", () => { - WindowsX509CertificateStore.ImportCertificateToStore( + new WindowsX509CertificateStore().ImportCertificateToStore( Convert.FromBase64String(sampleCertificate.Base64Bytes()), sampleCertificate.Password, StoreLocation.LocalMachine, "My", sampleCertificate.HasPrivateKey); }) .Concat(CreateThreads(numThreads, "AddPrivateKeyAccessRules", () => { - WindowsX509CertificateStore.AddPrivateKeyAccessRules( + new WindowsX509CertificateStore().AddPrivateKeyAccessRules( sampleCertificate.Thumbprint, StoreLocation.LocalMachine, "My", new List { @@ -169,7 +169,7 @@ public void CanImportCertificateForSpecificUser() sampleCertificate.EnsureCertificateNotInStore(storeName, StoreLocation.CurrentUser); - WindowsX509CertificateStore.ImportCertificateToStore(Convert.FromBase64String(sampleCertificate.Base64Bytes()), sampleCertificate.Password, + new WindowsX509CertificateStore().ImportCertificateToStore(Convert.FromBase64String(sampleCertificate.Base64Bytes()), sampleCertificate.Password, user, storeName, sampleCertificate.HasPrivateKey); sampleCertificate.AssertCertificateIsInStore(storeName, StoreLocation.CurrentUser); @@ -187,7 +187,7 @@ public void CanImportCertificateWithNoPrivateKeyForSpecificUser() sampleCertificate.EnsureCertificateNotInStore(storeName, StoreLocation.CurrentUser); - WindowsX509CertificateStore.ImportCertificateToStore(Convert.FromBase64String(sampleCertificate.Base64Bytes()), sampleCertificate.Password, + new WindowsX509CertificateStore().ImportCertificateToStore(Convert.FromBase64String(sampleCertificate.Base64Bytes()), sampleCertificate.Password, user, storeName, sampleCertificate.HasPrivateKey); sampleCertificate.AssertCertificateIsInStore(storeName, StoreLocation.CurrentUser); @@ -207,17 +207,17 @@ public void ImportExistingCertificateShouldNotOverwriteExistingPrivateKeyRights( sampleCertificate.EnsureCertificateNotInStore(storeName, storeLocation); - WindowsX509CertificateStore.ImportCertificateToStore( + new WindowsX509CertificateStore().ImportCertificateToStore( Convert.FromBase64String(sampleCertificate.Base64Bytes()), sampleCertificate.Password, storeLocation, storeName, sampleCertificate.HasPrivateKey); - WindowsX509CertificateStore.AddPrivateKeyAccessRules(sampleCertificate.Thumbprint, storeLocation, storeName, + new WindowsX509CertificateStore().AddPrivateKeyAccessRules(sampleCertificate.Thumbprint, storeLocation, storeName, new List { new PrivateKeyAccessRule("BUILTIN\\Users", PrivateKeyAccess.FullControl) }); - WindowsX509CertificateStore.ImportCertificateToStore( + new WindowsX509CertificateStore().ImportCertificateToStore( Convert.FromBase64String(sampleCertificate.Base64Bytes()), sampleCertificate.Password, storeLocation, storeName, sampleCertificate.HasPrivateKey); @@ -248,7 +248,7 @@ public void CanImportCertificateChain(string sampleCertificateId, string interme sampleCertificate.EnsureCertificateNotInStore(storeName, storeLocation); - WindowsX509CertificateStore.ImportCertificateToStore(Convert.FromBase64String(sampleCertificate.Base64Bytes()), sampleCertificate.Password, + new WindowsX509CertificateStore().ImportCertificateToStore(Convert.FromBase64String(sampleCertificate.Base64Bytes()), sampleCertificate.Password, storeLocation, storeName, sampleCertificate.HasPrivateKey); sampleCertificate.AssertCertificateIsInStore(storeName, storeLocation); diff --git a/source/Calamari/Commands/DeployPackageCommand.cs b/source/Calamari/Commands/DeployPackageCommand.cs index e5b161244..0e8ab997f 100644 --- a/source/Calamari/Commands/DeployPackageCommand.cs +++ b/source/Calamari/Commands/DeployPackageCommand.cs @@ -24,7 +24,7 @@ using Calamari.Deployment; using Calamari.Deployment.Conventions; using Calamari.Deployment.Features; -using Calamari.Deployment.PackageRetention; +using Calamari.Integration.Certificates; using Calamari.Integration.Iis; using Calamari.Integration.Nginx; @@ -42,6 +42,7 @@ public class DeployPackageCommand : Command readonly IExtractPackage extractPackage; readonly IStructuredConfigVariablesService structuredConfigVariablesService; readonly IDeploymentJournalWriter deploymentJournalWriter; + readonly IWindowsX509CertificateStore windowsX509CertificateStore; PathToPackage pathToPackage; public DeployPackageCommand( @@ -53,7 +54,8 @@ public DeployPackageCommand( ISubstituteInFiles substituteInFiles, IExtractPackage extractPackage, IStructuredConfigVariablesService structuredConfigVariablesService, - IDeploymentJournalWriter deploymentJournalWriter) + IDeploymentJournalWriter deploymentJournalWriter, + IWindowsX509CertificateStore windowsX509CertificateStore) { Options.Add("package=", "Path to the deployment package to install.", v => pathToPackage = new PathToPackage(Path.GetFullPath(v))); @@ -66,6 +68,7 @@ public DeployPackageCommand( this.extractPackage = extractPackage; this.structuredConfigVariablesService = structuredConfigVariablesService; this.deploymentJournalWriter = deploymentJournalWriter; + this.windowsX509CertificateStore = windowsX509CertificateStore; } public override int Execute(string[] commandLineArguments) @@ -87,7 +90,7 @@ public override int Execute(string[] commandLineArguments) var embeddedResources = new AssemblyEmbeddedResources(); #if IIS_SUPPORT var iis = new InternetInformationServer(); - featureClasses.AddRange(new IFeature[] { new IisWebSiteBeforeDeployFeature(), new IisWebSiteAfterPostDeployFeature() }); + featureClasses.AddRange(new IFeature[] { new IisWebSiteBeforeDeployFeature(windowsX509CertificateStore), new IisWebSiteAfterPostDeployFeature(windowsX509CertificateStore) }); #endif if (!CalamariEnvironment.IsRunningOnWindows) { diff --git a/source/Calamari/Commands/ImportCertificateCommand.cs b/source/Calamari/Commands/ImportCertificateCommand.cs index 7b6714d8f..126bd68da 100644 --- a/source/Calamari/Commands/ImportCertificateCommand.cs +++ b/source/Calamari/Commands/ImportCertificateCommand.cs @@ -9,17 +9,18 @@ using Calamari.Common.Plumbing.Variables; using Calamari.Deployment; using Calamari.Integration.Certificates; - namespace Calamari.Commands { [Command("import-certificate", Description = "Imports a X.509 certificate into a Windows certificate store")] public class ImportCertificateCommand : Command { readonly IVariables variables; - - public ImportCertificateCommand(IVariables variables) + readonly IWindowsX509CertificateStore windowsX509CertificateStore; + + public ImportCertificateCommand(IVariables variables, IWindowsX509CertificateStore windowsX509CertificateStore) { this.variables = variables; + this.windowsX509CertificateStore = windowsX509CertificateStore; } public override int Execute(string[] commandLineArguments) @@ -50,16 +51,16 @@ void ImportCertificate() { Log.Info( $"Importing certificate '{variables.Get($"{certificateVariable}.{CertificateVariables.Properties.Subject}")}' with thumbprint '{thumbprint}' into store '{storeLocation}\\{storeName}'"); - WindowsX509CertificateStore.ImportCertificateToStore(pfxBytes, password, storeLocation, storeName, - privateKeyExportable); + windowsX509CertificateStore.ImportCertificateToStore(pfxBytes, password, storeLocation, storeName, + privateKeyExportable); if (storeLocation == StoreLocation.LocalMachine) { // Set private-key access var privateKeyAccessRules = GetPrivateKeyAccessRules(variables); if (privateKeyAccessRules.Any()) - WindowsX509CertificateStore.AddPrivateKeyAccessRules(thumbprint, storeLocation, storeName, - privateKeyAccessRules); + windowsX509CertificateStore.AddPrivateKeyAccessRules(thumbprint, storeLocation, storeName, + privateKeyAccessRules); } } else // Import into a specific user's store @@ -74,8 +75,8 @@ void ImportCertificate() Log.Info( $"Importing certificate '{variables.Get($"{certificateVariable}.{CertificateVariables.Properties.Subject}")}' with thumbprint '{thumbprint}' into store '{storeName}' for user '{storeUser}'"); - WindowsX509CertificateStore.ImportCertificateToStore(pfxBytes, password, storeUser, storeName, - privateKeyExportable); + windowsX509CertificateStore.ImportCertificateToStore(pfxBytes, password, storeUser, storeName, + privateKeyExportable); } } @@ -122,10 +123,10 @@ static void ValidateStore(StoreLocation? storeLocation, string storeName) // Windows wants to launch an interactive confirmation dialog when importing into the Root store for a user. // https://github.com/OctopusDeploy/Issues/issues/3347 if ((!storeLocation.HasValue || storeLocation.Value != StoreLocation.LocalMachine) - && storeName == WindowsX509CertificateStore.RootAuthorityStoreName) + && storeName == WindowsX509CertificateConstants.RootAuthorityStoreName) { - throw new CommandException($"When importing certificate into {WindowsX509CertificateStore.RootAuthorityStoreName} store, location must be '{StoreLocation.LocalMachine}'. " + - $"Windows security restrictions prevent importing into the {WindowsX509CertificateStore.RootAuthorityStoreName} store for a user."); + throw new CommandException($"When importing certificate into {WindowsX509CertificateConstants.RootAuthorityStoreName} store, location must be '{StoreLocation.LocalMachine}'. " + + $"Windows security restrictions prevent importing into the {WindowsX509CertificateConstants.RootAuthorityStoreName} store for a user."); } } } diff --git a/source/Calamari/Deployment/Features/IisWebSiteAfterPostDeployFeature.cs b/source/Calamari/Deployment/Features/IisWebSiteAfterPostDeployFeature.cs index f329eb5db..11a271a93 100644 --- a/source/Calamari/Deployment/Features/IisWebSiteAfterPostDeployFeature.cs +++ b/source/Calamari/Deployment/Features/IisWebSiteAfterPostDeployFeature.cs @@ -13,24 +13,27 @@ namespace Calamari.Deployment.Features { public class IisWebSiteAfterPostDeployFeature : IisWebSiteFeature { + readonly IWindowsX509CertificateStore windowsX509CertificateStore; public override string DeploymentStage => DeploymentStages.AfterPostDeploy; + public IisWebSiteAfterPostDeployFeature(IWindowsX509CertificateStore windowsX509CertificateStore) + { + this.windowsX509CertificateStore = windowsX509CertificateStore; + } + public override void Execute(RunningDeployment deployment) { var variables = deployment.Variables; if (variables.GetFlag(SpecialVariables.Action.IisWebSite.DeployAsWebSite, false)) { -#if WINDOWS_CERTIFICATE_STORE_SUPPORT // For any bindings using certificate variables, the application pool account // must have access to the private-key. EnsureApplicationPoolHasCertificatePrivateKeyAccess(variables); -#endif } } -#if WINDOWS_CERTIFICATE_STORE_SUPPORT - static void EnsureApplicationPoolHasCertificatePrivateKeyAccess(IVariables variables) + void EnsureApplicationPoolHasCertificatePrivateKeyAccess(IVariables variables) { foreach (var binding in GetEnabledBindings(variables)) { @@ -42,7 +45,7 @@ static void EnsureApplicationPoolHasCertificatePrivateKeyAccess(IVariables varia var thumbprint = variables.Get($"{certificateVariable}.{CertificateVariables.Properties.Thumbprint}"); var privateKeyAccess = CreatePrivateKeyAccessForApplicationPoolAccount(variables); - WindowsX509CertificateStore.AddPrivateKeyAccessRules(thumbprint, + windowsX509CertificateStore.AddPrivateKeyAccessRules(thumbprint, StoreLocation.LocalMachine, new List { privateKeyAccess }); } @@ -66,6 +69,13 @@ static PrivateKeyAccessRule CreatePrivateKeyAccessForApplicationPoolAccount(IVar static IdentityReference GetIdentityForApplicationPoolIdentity(ApplicationPoolIdentityType applicationPoolIdentityType, IVariables variables) { + //TODO: Once this only runs netcore we can remove this check (or potentially externalize the whole check) +#if !NETFRAMEWORK + if (!OperatingSystem.IsWindows()) + { + throw new InvalidOperationException("This code should only be reachable on Windows Platforms"); + } +#endif switch (applicationPoolIdentityType) { case ApplicationPoolIdentityType.ApplicationPoolIdentity: @@ -95,6 +105,5 @@ static string StripLocalAccountIdentifierFromUsername(string username) //The following expression is to remove .\ from the beginning of usernames, we still allow for usernames in the format of machine\user or domain\user return Regex.Replace(username, "\\.\\\\(.*)", "$1", RegexOptions.None); } -#endif } } \ No newline at end of file diff --git a/source/Calamari/Deployment/Features/IisWebSiteBeforeDeployFeature.cs b/source/Calamari/Deployment/Features/IisWebSiteBeforeDeployFeature.cs index b8c5f9e93..62e7737a9 100644 --- a/source/Calamari/Deployment/Features/IisWebSiteBeforeDeployFeature.cs +++ b/source/Calamari/Deployment/Features/IisWebSiteBeforeDeployFeature.cs @@ -11,27 +11,28 @@ namespace Calamari.Deployment.Features { public class IisWebSiteBeforeDeployFeature : IisWebSiteFeature { + readonly IWindowsX509CertificateStore windowsX509CertificateStore; public override string DeploymentStage => DeploymentStages.BeforeDeploy; + public IisWebSiteBeforeDeployFeature(IWindowsX509CertificateStore windowsX509CertificateStore) + { + this.windowsX509CertificateStore = windowsX509CertificateStore; + } + public override void Execute(RunningDeployment deployment) { var variables = deployment.Variables; if (variables.GetFlag(SpecialVariables.Action.IisWebSite.DeployAsWebSite, false)) { - -#if WINDOWS_CERTIFICATE_STORE_SUPPORT // Any certificate-variables used by IIS bindings must be placed in the // LocalMachine certificate store EnsureCertificatesUsedInBindingsAreInStore(variables); -#endif - } } -#if WINDOWS_CERTIFICATE_STORE_SUPPORT - static void EnsureCertificatesUsedInBindingsAreInStore(IVariables variables) + void EnsureCertificatesUsedInBindingsAreInStore(IVariables variables) { foreach (var binding in GetEnabledBindings(variables)) { @@ -44,11 +45,11 @@ static void EnsureCertificatesUsedInBindingsAreInStore(IVariables variables) } } - static void EnsureCertificateInStore(IVariables variables, string certificateVariable) + void EnsureCertificateInStore(IVariables variables, string certificateVariable) { var thumbprint = variables.Get($"{certificateVariable}.{CertificateVariables.Properties.Thumbprint}"); - var storeName = WindowsX509CertificateStore.FindCertificateStore(thumbprint, StoreLocation.LocalMachine); + var storeName = windowsX509CertificateStore.FindCertificateStore(thumbprint, StoreLocation.LocalMachine); if (storeName != null) { Log.Verbose($"Found existing certificate with thumbprint '{thumbprint}' in Cert:\\LocalMachine\\{storeName}"); @@ -66,9 +67,7 @@ static void EnsureCertificateInStore(IVariables variables, string certificateVar Log.SetOutputVariable(SpecialVariables.Action.IisWebSite.Output.CertificateStoreName, storeNamesVariable, variables); } - - - static string AddCertificateToLocalMachineStore(IVariables variables, string certificateVariable) + string AddCertificateToLocalMachineStore(IVariables variables, string certificateVariable) { var pfxBytes = Convert.FromBase64String(variables.Get($"{certificateVariable}.{CertificateVariables.Properties.Pfx}")); var password = variables.Get($"{certificateVariable}.{CertificateVariables.Properties.Password}"); @@ -78,7 +77,7 @@ static string AddCertificateToLocalMachineStore(IVariables variables, string cer try { - WindowsX509CertificateStore.ImportCertificateToStore(pfxBytes, password, StoreLocation.LocalMachine, "My", true); + windowsX509CertificateStore.ImportCertificateToStore(pfxBytes, password, StoreLocation.LocalMachine, "My", true); return "My"; } catch (Exception) @@ -87,7 +86,7 @@ static string AddCertificateToLocalMachineStore(IVariables variables, string cer throw; } } -#endif + } } \ No newline at end of file diff --git a/source/Calamari/Program.cs b/source/Calamari/Program.cs index 1f48babe3..450683093 100644 --- a/source/Calamari/Program.cs +++ b/source/Calamari/Program.cs @@ -71,6 +71,14 @@ protected override void ConfigureContainer(ContainerBuilder builder, CommonOptio builder.RegisterType().As().SingleInstance(); builder.RegisterType().AsSelf(); builder.RegisterType().As().SingleInstance(); + + //TODO: Once this runs on both netcore and full framework, this can be converted to a runtime conditional check +#if WINDOWS_CERTIFICATE_STORE_SUPPORT + builder.RegisterType().As().SingleInstance(); +#else + builder.RegisterType().As().SingleInstance(); +#endif + builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); From 385ceab097791a6aace0e7d4e293e0935df686a8 Mon Sep 17 00:00:00 2001 From: Robert E Date: Thu, 25 Jul 2024 16:33:20 +1000 Subject: [PATCH 2/8] extraction of IIS functionality into a legacy executable --- .../FeatureToggles/OctopusFeatureToggle.cs | 1 + .../Calamari.FullFrameworkTools.Tests.csproj | 55 +++ .../Command/CommandRequestInvokerTests.cs | 142 ++++++++ .../Iis/IisFixture.cs | 66 ++++ .../IisCommandTests.cs | 41 +++ .../Properties/AssemblyInfo.cs | 3 + .../Calamari.FullFrameworkTools.csproj | 40 +++ .../Command/AesEncryption.cs | 115 ++++++ .../Command/CommandLocator.cs | 46 +++ .../Command/CommandRequestInvoker.cs | 48 +++ .../Command/ICommand.cs | 31 ++ .../Command/ILog.cs | 12 + .../ImportCertificateToStoreHandler.cs | 29 ++ .../Command/Log.cs | 61 ++++ .../Command/OverwriteHomeDirectoryHandler.cs | 34 ++ .../ExceptionExtensions.cs | 26 ++ .../IFullFrameworkCommand.cs | 13 + .../Iis/ApplicationPoolIdentityType.cs | 13 + .../Iis/IInternetInformationServer.cs | 16 + .../Iis/IisCommand.cs | 57 +++ .../Iis/InternetInformationServer.cs | 35 ++ .../Iis/WebServerSevenSupport.cs | 221 ++++++++++++ .../Iis/WebServerSixSupport.cs | 202 +++++++++++ .../Iis/WebServerSupport.cs | 33 ++ source/Calamari.FullFrameworkTools/Program.cs | 79 ++++ .../Properties/AssemblyInfo.cs | 9 + .../WindowsNative/CertificatePal.cs | 198 +++++++++++ .../WindowsNative/SafeCertContextHandle.cs | 61 ++++ .../SafeCertContextHandleExtensions.cs | 30 ++ .../WindowsNative/SafeCertStoreHandle.cs | 32 ++ .../WindowsNative/SafeCspHandle.cs | 66 ++++ .../WindowsNative/WindowsX509Native.cs | 336 ++++++++++++++++++ .../WindowsX509CertificateStore.cs | 1 + .../Calamari.Tests/Fixtures/Iis/IisFixture.cs | 5 + source/Calamari.sln | 12 + source/Calamari/Calamari.csproj | 1 + .../Calamari/Commands/DeployPackageCommand.cs | 5 +- ...lFrameworkToolInternetInformationServer.cs | 23 ++ .../Iis/InternetInformationServer.cs | 5 +- 39 files changed, 2199 insertions(+), 4 deletions(-) create mode 100644 source/Calamari.FullFrameworkTools.Tests/Calamari.FullFrameworkTools.Tests.csproj create mode 100644 source/Calamari.FullFrameworkTools.Tests/Command/CommandRequestInvokerTests.cs create mode 100644 source/Calamari.FullFrameworkTools.Tests/Iis/IisFixture.cs create mode 100644 source/Calamari.FullFrameworkTools.Tests/IisCommandTests.cs create mode 100644 source/Calamari.FullFrameworkTools.Tests/Properties/AssemblyInfo.cs create mode 100644 source/Calamari.FullFrameworkTools/Calamari.FullFrameworkTools.csproj create mode 100644 source/Calamari.FullFrameworkTools/Command/AesEncryption.cs create mode 100644 source/Calamari.FullFrameworkTools/Command/CommandLocator.cs create mode 100644 source/Calamari.FullFrameworkTools/Command/CommandRequestInvoker.cs create mode 100644 source/Calamari.FullFrameworkTools/Command/ICommand.cs create mode 100644 source/Calamari.FullFrameworkTools/Command/ILog.cs create mode 100644 source/Calamari.FullFrameworkTools/Command/ImportCertificateToStoreHandler.cs create mode 100644 source/Calamari.FullFrameworkTools/Command/Log.cs create mode 100644 source/Calamari.FullFrameworkTools/Command/OverwriteHomeDirectoryHandler.cs create mode 100644 source/Calamari.FullFrameworkTools/ExceptionExtensions.cs create mode 100644 source/Calamari.FullFrameworkTools/IFullFrameworkCommand.cs create mode 100644 source/Calamari.FullFrameworkTools/Iis/ApplicationPoolIdentityType.cs create mode 100644 source/Calamari.FullFrameworkTools/Iis/IInternetInformationServer.cs create mode 100644 source/Calamari.FullFrameworkTools/Iis/IisCommand.cs create mode 100644 source/Calamari.FullFrameworkTools/Iis/InternetInformationServer.cs create mode 100644 source/Calamari.FullFrameworkTools/Iis/WebServerSevenSupport.cs create mode 100644 source/Calamari.FullFrameworkTools/Iis/WebServerSixSupport.cs create mode 100644 source/Calamari.FullFrameworkTools/Iis/WebServerSupport.cs create mode 100644 source/Calamari.FullFrameworkTools/Program.cs create mode 100644 source/Calamari.FullFrameworkTools/Properties/AssemblyInfo.cs create mode 100644 source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/CertificatePal.cs create mode 100644 source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCertContextHandle.cs create mode 100644 source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCertContextHandleExtensions.cs create mode 100644 source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCertStoreHandle.cs create mode 100644 source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCspHandle.cs create mode 100644 source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/WindowsX509Native.cs create mode 100644 source/Calamari/Integration/Iis/InProcessFullFrameworkToolInternetInformationServer.cs diff --git a/source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs b/source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs index 20fa72cd8..abfb67b3b 100644 --- a/source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs +++ b/source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs @@ -5,6 +5,7 @@ namespace Calamari.Common.FeatureToggles public static class OctopusFeatureToggles { public static readonly OctopusFeatureToggle NonPrimaryGitDependencySupportFeatureToggle = new OctopusFeatureToggle("non-primary-git-dependency-support"); + public static readonly OctopusFeatureToggle FullFrameworkTasksExternalProcess = new OctopusFeatureToggle("full-framework-tasks-external-process"); public class OctopusFeatureToggle { diff --git a/source/Calamari.FullFrameworkTools.Tests/Calamari.FullFrameworkTools.Tests.csproj b/source/Calamari.FullFrameworkTools.Tests/Calamari.FullFrameworkTools.Tests.csproj new file mode 100644 index 000000000..6872b8775 --- /dev/null +++ b/source/Calamari.FullFrameworkTools.Tests/Calamari.FullFrameworkTools.Tests.csproj @@ -0,0 +1,55 @@ + + + true + Calamari.FullFrameworkTools.Tests + Calamari.FullFrameworkTools.Tests + true + Library + false + NU1603 + + + net462 + Calamari.FullFrameworkTools.Tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/Calamari.FullFrameworkTools.Tests/Command/CommandRequestInvokerTests.cs b/source/Calamari.FullFrameworkTools.Tests/Command/CommandRequestInvokerTests.cs new file mode 100644 index 000000000..51d4772d5 --- /dev/null +++ b/source/Calamari.FullFrameworkTools.Tests/Command/CommandRequestInvokerTests.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using Calamari.FullFrameworkTools.Command; +using FluentAssertions; +using NSubstitute; +using NUnit.Framework; + +namespace Calamari.FullFrameworkTools.Tests.Command +{ + [TestFixture] + public class CommandRequestInvokerTests + { + readonly ICommandLocator commandLocator; + + public CommandRequestInvokerTests() + { + commandLocator = Substitute.For(); + } + + [Test] + public void SimpleHandlerReturnsSerializedResult() + { + var handler = new TestAdditionCommandHandler(); + commandLocator.GetCommand(Arg.Any()).Returns(handler); + var requestObject = new TestAdditionRequest(4, 5); + var jsonRequestObj = JsonSerializer.Serialize(requestObject, new JsonSerializerOptions()); + + var invoker = new CommandRequestInvoker(commandLocator); + var response = invoker.Run("TestCommand", jsonRequestObj); + + response.Should().BeEquivalentTo(new TestAdditionResponse(9)); + } + + [Test] + public void SimpleHandlerEncrypted() + { + var handler = new TestAdditionCommandHandler(); + commandLocator.GetCommand(Arg.Any()).Returns(handler); + var requestObject = new TestAdditionRequest(4, 5); + var jsonRequestObj = JsonSerializer.Serialize(requestObject, new JsonSerializerOptions()); + + var password = "pass23HJka"; + var enc = new AesEncryption(password); + var encRequestObj = enc.Encrypt(jsonRequestObj); + + using (var temp = new TempFile(encRequestObj)) + { + var invoker = new CommandRequestInvoker(commandLocator); + var response = invoker.Run("TestCommand", password, temp.FilePath); + + response.Should().BeEquivalentTo(new TestAdditionResponse(9)); + } + } + + public class TempFile : IDisposable + { + public TempFile(byte[] content) + { + FilePath = Path.GetTempFileName(); + File.WriteAllBytes(FilePath, content); + } + + public string FilePath { get; } + + public void Dispose() + { + try + { + File.Delete(FilePath); + } + catch + { + // ignored + } + } + } + + public class TestLog : ILog + { + public List Errors { get; } = new List(); + public List Infos { get; } = new List(); + + public List Fatals { get; } = new List(); + + public void Verbose(string message) + { + throw new NotImplementedException(); + } + + public void Error(string value) + { + Errors.Add(value); + } + + public void Info(string value) + { + Infos.Add(value); + } + + public void Fatal(Exception exception) + { + Fatals.Add(exception); + } + + public void Result(object response) + { + } + } + + class TestAdditionCommandHandler : FullFrameworkToolCommandHandler + { + protected override TestAdditionResponse Handle(TestAdditionRequest additionRequest) + { + return new TestAdditionResponse(additionRequest.NumberOne + additionRequest.NumberTwo); + } + + } + + class TestAdditionRequest: IFullFrameworkToolRequest + { + public int NumberOne { get; } + public int NumberTwo { get; } + + public TestAdditionRequest(int numberOne, int numberTwo) + { + NumberOne = numberOne; + NumberTwo = numberTwo; + } + } + class TestAdditionResponse: IFullFrameworkToolResponse + { + public int Result { get; } + + public TestAdditionResponse(int result) + { + Result = result; + } + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools.Tests/Iis/IisFixture.cs b/source/Calamari.FullFrameworkTools.Tests/Iis/IisFixture.cs new file mode 100644 index 000000000..45c9df21c --- /dev/null +++ b/source/Calamari.FullFrameworkTools.Tests/Iis/IisFixture.cs @@ -0,0 +1,66 @@ +using System; +using Calamari.FullFrameworkTools.Iis; +using NUnit.Framework; + +namespace Calamari.FullFrameworkTools.Tests.Iis +{ + /// + /// Note. This test is a direct clone of . + /// While we extract IIS functionality out of Calamari, we will have the relevant functionality in both places for a short period. + /// Ensure any changes to make to this test are reflected in the clone test. + /// + [TestFixture] + public class IisFixture + { + readonly WebServerSupport webServer = WebServerSupport.AutoDetect(); + string siteName; + + [SetUp] + public void SetUp() + { + siteName = "Test-" + Guid.NewGuid(); + webServer.CreateWebSiteOrVirtualDirectory(siteName, "/", "C:\\InetPub\\wwwroot", 1081); + webServer.CreateWebSiteOrVirtualDirectory(siteName, "/Foo", "C:\\InetPub\\wwwroot", 1081); + webServer.CreateWebSiteOrVirtualDirectory(siteName, "/Foo/Bar/Baz", "C:\\InetPub\\wwwroot", 1081); + } + + [Test] + public void CanUpdateIisSite() + { + var server = new InternetInformationServer(); + var success = server.OverwriteHomeDirectory(siteName, "C:\\Windows\\system32", false); + Assert.IsTrue(success, "Home directory was not overwritten"); + + var path = webServer.GetHomeDirectory(siteName, "/"); + Assert.AreEqual("C:\\Windows\\system32", path); + } + + [Test] + public void CanUpdateIisSiteWithVirtualDirectory() + { + var server = new InternetInformationServer(); + var success = server.OverwriteHomeDirectory(siteName + "/Foo", "C:\\Windows\\Microsoft.NET", false); + Assert.IsTrue(success, "Home directory was not overwritten"); + + var path = webServer.GetHomeDirectory(siteName, "/Foo"); + Assert.AreEqual("C:\\Windows\\Microsoft.NET", path); + } + + [Test] + public void CanUpdateIisSiteWithNestedVirtualDirectory() + { + var server = new InternetInformationServer(); + var success = server.OverwriteHomeDirectory(siteName + "/Foo/Bar/Baz", "C:\\Windows\\Microsoft.NET\\Framework", false); + Assert.IsTrue(success, "Home directory was not overwritten"); + + var path = webServer.GetHomeDirectory(siteName, "/Foo/Bar/Baz"); + Assert.AreEqual("C:\\Windows\\Microsoft.NET\\Framework", path); + } + + [TearDown] + public void TearDown() + { + webServer.DeleteWebSite(siteName); + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools.Tests/IisCommandTests.cs b/source/Calamari.FullFrameworkTools.Tests/IisCommandTests.cs new file mode 100644 index 000000000..80ffe2a68 --- /dev/null +++ b/source/Calamari.FullFrameworkTools.Tests/IisCommandTests.cs @@ -0,0 +1,41 @@ +using System; +using Calamari.FullFrameworkTools.Iis; +using NSubstitute; +using NUnit.Framework; + +namespace Calamari.FullFrameworkTools.Tests +{ + [TestFixture] + public class IisCommandTests + { + [Test] + public void GivenTwoParameters_ThenParametersPassedToOverwriteHomeDirectory() + { + var iisServer = Substitute.For(); + var cmd = new IisCommand(iisServer); + + var websiteName = "WEBSITENAME"; + var path = "PATH"; + cmd.Execute(new[] { websiteName, path }); + iisServer.Received().OverwriteHomeDirectory(websiteName, path, false); + } + + [Test] + public void GivenOneParameter_ThenException() + { + var iisServer = Substitute.For(); + var cmd = new IisCommand(iisServer); + + Assert.Throws(() => cmd.Execute(new string[] { Guid.NewGuid().ToString()})); + } + + [Test] + public void GivenNoParameter_ThenException() + { + var iisServer = Substitute.For(); + var cmd = new IisCommand(iisServer); + + Assert.Throws(() => cmd.Execute(Array.Empty())); + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools.Tests/Properties/AssemblyInfo.cs b/source/Calamari.FullFrameworkTools.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..bf3bb0167 --- /dev/null +++ b/source/Calamari.FullFrameworkTools.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.InteropServices; + +[assembly: Guid("EC26DBFD-A364-4AF9-BFEF-F8ABCA2656FA")] diff --git a/source/Calamari.FullFrameworkTools/Calamari.FullFrameworkTools.csproj b/source/Calamari.FullFrameworkTools/Calamari.FullFrameworkTools.csproj new file mode 100644 index 000000000..bf5ad538f --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Calamari.FullFrameworkTools.csproj @@ -0,0 +1,40 @@ + + + 1.0.0.0 + true + Calamari.FullFrameworkTools + Calamari.FullFrameworkTools + Calamari.FullFrameworkTools + Exe + Calamari.FullFrameworkTools + false + https://github.com/OctopusDeploy/Calamari + https://github.com/OctopusDeploy/Calamari/blob/main/LICENSE.txt + Octopus Deploy + Octopus Deploy Pty Ltd + tool + git + https://github.com/OctopusDeploy/Calamari/ + true + Contains the command-line Calamari tool that is used by Tentacle to perform depoyment actions on machines. + net462 + latest + + + $(DefineConstants);IIS_SUPPORT;WINDOWS_CERTIFICATE_STORE_SUPPORT + anycpu + + + $(DefineConstants);DEBUG + + + + + + + + + + + + \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Command/AesEncryption.cs b/source/Calamari.FullFrameworkTools/Command/AesEncryption.cs new file mode 100644 index 000000000..119ac8c26 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Command/AesEncryption.cs @@ -0,0 +1,115 @@ +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace Calamari.FullFrameworkTools.Command +{ + public class AesEncryption + { + const int PasswordSaltIterations = 1000; + public const string SaltRaw = "Octopuss"; + static readonly byte[] PasswordPaddingSalt = Encoding.UTF8.GetBytes(SaltRaw); + static readonly byte[] IvPrefix = Encoding.UTF8.GetBytes("IV__"); + + static readonly Random RandomGenerator = new Random(); + + readonly byte[] key; + + public AesEncryption(string password) + { + key = GetEncryptionKey(password); + } + + public string Decrypt(byte[] encrypted) + { + byte[] iv; + var aesBytes = ExtractIV(encrypted, out iv); + using (var algorithm = GetCryptoProvider(iv)) + using (var dec = algorithm.CreateDecryptor()) + using (var ms = new MemoryStream(aesBytes)) + using (var cs = new CryptoStream(ms, dec, CryptoStreamMode.Read)) + using (var sr = new StreamReader(cs, Encoding.UTF8)) + { + return sr.ReadToEnd(); + } + } + + public byte[] Encrypt(string plaintext) + { + var plainTextBytes = Encoding.UTF8.GetBytes(plaintext); + using (var algorithm = GetCryptoProvider()) + using (var cryptoTransform = algorithm.CreateEncryptor()) + using (var stream = new MemoryStream()) + { + // The IV is randomly generated each time so safe to append + stream.Write(IvPrefix, 0, IvPrefix.Length); + stream.Write(algorithm.IV, 0, algorithm.IV.Length); + using (var cs = new CryptoStream(stream, cryptoTransform, CryptoStreamMode.Write)) + { + cs.Write(plainTextBytes, 0, plainTextBytes.Length); + } + + /* + For testing purposes + var key hex = BitConverter.ToString(algorithm.Key).Replace("-", string.Empty); + var iv hex = BitConverter.ToString(algorithm.IV).Replace("-", string.Empty); + var enc b64 = Convert.ToBase64String(stream.ToArray()); + */ + return stream.ToArray(); + } + } + + Aes GetCryptoProvider(byte[] iv = null) + { + var provider = new AesCryptoServiceProvider + { + Mode = CipherMode.CBC, + Padding = PaddingMode.PKCS7, + KeySize = 128, + BlockSize = 128, + Key = key + }; + if (iv != null) + provider.IV = iv; + return provider; + } + + public static byte[] ExtractIV(byte[] encrypted, out byte[] iv) + { + var ivLength = 16; + iv = new byte[ivLength]; + Buffer.BlockCopy(encrypted, + IvPrefix.Length, + iv, + 0, + ivLength); + + var ivDataLength = IvPrefix.Length + ivLength; + var aesDataLength = encrypted.Length - ivDataLength; + var aesData = new byte[aesDataLength]; + Buffer.BlockCopy(encrypted, + ivDataLength, + aesData, + 0, + aesDataLength); + return aesData; + } + + public static byte[] GetEncryptionKey(string encryptionPassword) + { + var passwordGenerator = new Rfc2898DeriveBytes(encryptionPassword, PasswordPaddingSalt, PasswordSaltIterations); + return passwordGenerator.GetBytes(16); + } + + public static string RandomString(int length) + { + const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + return new string( + Enumerable.Repeat(chars, length) + .Select(s => s[RandomGenerator.Next(s.Length)]) + .ToArray()); + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Command/CommandLocator.cs b/source/Calamari.FullFrameworkTools/Command/CommandLocator.cs new file mode 100644 index 000000000..512b5b3a3 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Command/CommandLocator.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Calamari.FullFrameworkTools.Command +{ + public interface ICommandLocator + { + IFullFrameworkToolCommandHandler GetCommand(string name); + } + + public class CommandLocator: ICommandLocator + { + public IList Commands = new List() + { + new ImportCertificateToStoreHandler(), + new OverwriteHomeDirectoryHandler() + }; + + public IFullFrameworkToolCommandHandler GetCommand(string name) + { + return Commands.FirstOrDefault(t => t.GetType().Name.Equals($"{name}Handler")); + } + + + public IFullFrameworkToolCommandHandler GetCommand() + { + return Commands.FirstOrDefault(t => GetAllTypes(t.GetType()).Contains(typeof(THandler))); + } + + public IEnumerable GetAllTypes(Type type) + { + yield return type; + + if (type.BaseType == null) + yield break; + + yield return type.BaseType; + foreach (var b in GetAllTypes(type.BaseType)) + { + yield return b; + } + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Command/CommandRequestInvoker.cs b/source/Calamari.FullFrameworkTools/Command/CommandRequestInvoker.cs new file mode 100644 index 000000000..507ef7413 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Command/CommandRequestInvoker.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using System.Text.Json; +using Calamari.FullFrameworkTools.Iis; + +namespace Calamari.FullFrameworkTools.Command +{ + public class CommandRequestInvoker + { + readonly ICommandLocator commandLocator; + + public CommandRequestInvoker(ICommandLocator commandLocator) + { + this.commandLocator = commandLocator; + } + + public object Run(string command, string content) + { + var commandHandler = commandLocator.GetCommand(command); + if (commandHandler == null) + { + throw new CommandException($"Unknown command {command}"); + } + + var requestType = commandHandler.GetType().BaseType.GetGenericArguments()[0]; //Probably proper type checking + var requestObject = JsonSerializer.Deserialize(content, requestType, new JsonSerializerOptions()); + return commandHandler.Handle(requestObject); + } + + public object Run(string command, string encryptionPassword, string filePath) + { + string rawContent; + + try + { + var encrypedContent = File.ReadAllBytes(filePath); + rawContent = new AesEncryption(encryptionPassword).Decrypt(encrypedContent); + } + catch (IOException ex) + { + throw new CommandException($"Unable to read file: {ex.Message}"); + } + + return Run(command, rawContent); + } + + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Command/ICommand.cs b/source/Calamari.FullFrameworkTools/Command/ICommand.cs new file mode 100644 index 000000000..fb5e47fb6 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Command/ICommand.cs @@ -0,0 +1,31 @@ +using System; + +namespace Calamari.FullFrameworkTools.Command +{ + public abstract class FullFrameworkToolCommandHandler : IFullFrameworkToolCommandHandler + where TFullFrameworkToolRequest : IFullFrameworkToolRequest + where TFullFrameworkToolResponse : IFullFrameworkToolResponse + + { + protected abstract TFullFrameworkToolResponse Handle(TFullFrameworkToolRequest request); + + public object Handle(object request) + { + return this.Handle((TFullFrameworkToolRequest)request); + } + } + + public interface IFullFrameworkToolRequest + { + } + + public interface IFullFrameworkToolResponse + { + } + + + public interface IFullFrameworkToolCommandHandler + { + object Handle(object request); + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Command/ILog.cs b/source/Calamari.FullFrameworkTools/Command/ILog.cs new file mode 100644 index 000000000..bdb0c0b31 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Command/ILog.cs @@ -0,0 +1,12 @@ +using System; + +namespace Calamari.FullFrameworkTools.Command; + +public interface ILog +{ + void Verbose(string message); + void Error(string message); + void Info(string message); + void Fatal(Exception exception); + void Result(object response); +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Command/ImportCertificateToStoreHandler.cs b/source/Calamari.FullFrameworkTools/Command/ImportCertificateToStoreHandler.cs new file mode 100644 index 000000000..f030c9abf --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Command/ImportCertificateToStoreHandler.cs @@ -0,0 +1,29 @@ +using System; +using System.Security.Cryptography.X509Certificates; + +namespace Calamari.FullFrameworkTools.Command +{ + public class ImportCertificateToStoreHandler : FullFrameworkToolCommandHandler + { + public string Name => "import-certificate-to-store"; + + protected override ImportCertificateToStoreResponse Handle(ImportCertificateToStoreRequest request) + { + throw new System.NotImplementedException(); + } + } + + public class ImportCertificateToStoreRequest : IFullFrameworkToolRequest + { + public byte[] pfxBytes { get; set; } + public string password { get; set; } + StoreLocation storeLocation { get; set; } + string storeName { get; set; } + string PrivateKeyExportable { get; set; } + } + + + public class ImportCertificateToStoreResponse : IFullFrameworkToolResponse + { + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Command/Log.cs b/source/Calamari.FullFrameworkTools/Command/Log.cs new file mode 100644 index 000000000..5287ea4f2 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Command/Log.cs @@ -0,0 +1,61 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Calamari.FullFrameworkTools.Command +{ + public class Log: ILog + { + private enum LogLevel + { + Verbose, + Info, + Warn, + Error, + Fatal, // Used for Exceptions + Result, // Special Response + } + + public void Verbose(string message) + { + var line = JsonSerializer.Serialize(new { Level = LogLevel.Verbose.ToString(), Message = message }); + Console.WriteLine(line); + } + + public void Error(string message) + { + var line = JsonSerializer.Serialize(new { Level = LogLevel.Error.ToString(), Message = message }); + Console.WriteLine(line); + } + + public void Fatal(Exception exception) + { + // For simplicity lets not assume Inner exceptions right now.... + var line = JsonSerializer.Serialize(new + { + Level = LogLevel.Fatal.ToString(), + Message = exception.Message, + Type = exception.GetType().Name, + StackTrace = exception.StackTrace + }); + Console.WriteLine(line); + } + + string Serialize(LogLevel level, string message) + { + return JsonSerializer.Serialize(new { Level = level.ToString(), Message = message}); + } + + public void Info(string message) + { + var line = JsonSerializer.Serialize(new { Level = LogLevel.Info, Message = message }); + Console.WriteLine(line); + } + + public void Result(object result) + { + var line = JsonSerializer.Serialize(new { Level = LogLevel.Result, Result = result}); + Console.WriteLine(line); + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Command/OverwriteHomeDirectoryHandler.cs b/source/Calamari.FullFrameworkTools/Command/OverwriteHomeDirectoryHandler.cs new file mode 100644 index 000000000..869a41302 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Command/OverwriteHomeDirectoryHandler.cs @@ -0,0 +1,34 @@ +using System; +using Calamari.FullFrameworkTools.Iis; + +namespace Calamari.FullFrameworkTools.Command +{ + public class OverwriteHomeDirectoryHandler : FullFrameworkToolCommandHandler + { + public string Name { get; } + + protected override OverwriteHomeDirectoryResponse Handle(OverwriteHomeDirectoryRequest request) + { + var iis = new InternetInformationServer(); + var result = iis.OverwriteHomeDirectory(request.IisWebSiteName, request.Path, request.LegacySupport); + return new OverwriteHomeDirectoryResponse(result); + } + } + + public class OverwriteHomeDirectoryRequest : IFullFrameworkToolRequest + { + public string IisWebSiteName { get; set; } + public string Path { get; set; } + public bool LegacySupport { get; set; } + } + + public class OverwriteHomeDirectoryResponse : IFullFrameworkToolResponse + { + public OverwriteHomeDirectoryResponse(bool result) + { + Result = result; + } + + public bool Result { get; set; } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/ExceptionExtensions.cs b/source/Calamari.FullFrameworkTools/ExceptionExtensions.cs new file mode 100644 index 000000000..bfdd7b4a5 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/ExceptionExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Text; + +namespace Calamari.FullFrameworkTools +{ + public static class ExceptionExtensions + { + public static string PrettyPrint(this Exception ex, StringBuilder sb = null) + { + sb ??= new StringBuilder(); + + sb.AppendLine(ex.Message); + sb.AppendLine(ex.GetType().FullName); + sb.AppendLine(ex.StackTrace); + + if (ex.InnerException != null) + { + sb.AppendLine(); + sb.AppendLine("--Inner Exception--"); + PrettyPrint(ex.InnerException, sb); + } + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/IFullFrameworkCommand.cs b/source/Calamari.FullFrameworkTools/IFullFrameworkCommand.cs new file mode 100644 index 000000000..f19fe1d16 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/IFullFrameworkCommand.cs @@ -0,0 +1,13 @@ +using System; + +namespace Calamari.FullFrameworkTools +{ + public interface IFullFrameworkCommand + { + string Name { get; } + + string Execute(string[] args); + + string WriteHelp(); + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Iis/ApplicationPoolIdentityType.cs b/source/Calamari.FullFrameworkTools/Iis/ApplicationPoolIdentityType.cs new file mode 100644 index 000000000..57a417f65 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Iis/ApplicationPoolIdentityType.cs @@ -0,0 +1,13 @@ +using System; + +namespace Calamari.FullFrameworkTools.Iis +{ + public enum ApplicationPoolIdentityType + { + ApplicationPoolIdentity, + LocalService, + LocalSystem, + NetworkService, + SpecificUser + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Iis/IInternetInformationServer.cs b/source/Calamari.FullFrameworkTools/Iis/IInternetInformationServer.cs new file mode 100644 index 000000000..64cdd604f --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Iis/IInternetInformationServer.cs @@ -0,0 +1,16 @@ +using System; + +namespace Calamari.FullFrameworkTools.Iis +{ + public interface IInternetInformationServer + { + /// + /// Sets the home directory (web root) of the given IIS website to the given path. + /// + /// The name of the web site under IIS. + /// The path to point the site to. + /// If true, forces using the IIS6 compatible IIS support. Otherwise, try to auto-detect. + /// True if the IIS site was found and updated. False if it could not be found. + bool OverwriteHomeDirectory(string iisWebSiteName, string path, bool legacySupport); + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Iis/IisCommand.cs b/source/Calamari.FullFrameworkTools/Iis/IisCommand.cs new file mode 100644 index 000000000..e87dadb28 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Iis/IisCommand.cs @@ -0,0 +1,57 @@ +using System; + +namespace Calamari.FullFrameworkTools.Iis +{ + public class CommandException : Exception + { + public CommandException(string message): base(message) + { + } + } + /// + /// Command takes the form of + /// + public class IisCommand : IFullFrameworkCommand + { + readonly IInternetInformationServer iisServer; + + public IisCommand(IInternetInformationServer iisServer) + { + this.iisServer = iisServer; + } + public string Name => "overwrite-home-directory"; + public string Execute(string[] args) + { + TryExtractArgs(args, + out var iisWebSiteNameAndVirtualDirectory, + out var path, + out var legacySupport); + + var result = iisServer.OverwriteHomeDirectory(iisWebSiteNameAndVirtualDirectory, path, legacySupport); + return $"{{\"result\": {result.ToString().ToLower()}}}"; + } + + public string WriteHelp() + { + return $"Calamari.FulleFrameworkTools.exe {Name} []"; + } + + static void TryExtractArgs(object[] args, out string iisWebSiteNameAndVirtualDirectory, out string path, out bool legacySupport) + { + if (args.Length < 2) + { + throw new CommandException("Missing Arguments"); + } + iisWebSiteNameAndVirtualDirectory = args[0].ToString(); + path = args[1].ToString(); + legacySupport = false; + if (args.Length == 3) + { + if (!bool.TryParse(args[2].ToString(), out legacySupport)) + { + throw new CommandException("Invalid Argument"); + } + } + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Iis/InternetInformationServer.cs b/source/Calamari.FullFrameworkTools/Iis/InternetInformationServer.cs new file mode 100644 index 000000000..211cbd6ea --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Iis/InternetInformationServer.cs @@ -0,0 +1,35 @@ +using System; +using System.Linq; + +namespace Calamari.FullFrameworkTools.Iis +{ + /// + /// Tools for working with IIS. + /// + public class InternetInformationServer : IInternetInformationServer + { + + /// + /// Sets the home directory (web root) of the given IIS website to the given path. + /// + /// The name of the web site under IIS. + /// The path to point the site to. + /// If true, forces using the IIS6 compatible IIS support. Otherwise, try to auto-detect. + /// + /// True if the IIS site was found and updated. False if it could not be found. + /// + public bool OverwriteHomeDirectory(string iisWebSiteNameAndVirtualDirectory, string path, bool legacySupport) + { + var parts = iisWebSiteNameAndVirtualDirectory.Split('/'); + var iisSiteName = parts.First(); + var remainder = parts.Skip(1).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + var virtualDirectory = remainder.Length > 0 ? string.Join("/", remainder) : null; + + var server = legacySupport + ? WebServerSupport.Legacy() + : WebServerSupport.AutoDetect(); + + return server.ChangeHomeDirectory(iisSiteName, virtualDirectory, path); + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Iis/WebServerSevenSupport.cs b/source/Calamari.FullFrameworkTools/Iis/WebServerSevenSupport.cs new file mode 100644 index 000000000..2fb2180c4 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Iis/WebServerSevenSupport.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Web.Administration; + +namespace Calamari.FullFrameworkTools.Iis +{ + public class WebServerSevenSupport : WebServerSupport + { + const string Localhost = "localhost"; + + public override void CreateWebSiteOrVirtualDirectory(string webSiteName, string virtualDirectoryPath, string webRootPath, int port) + { + var virtualParts = (virtualDirectoryPath ?? String.Empty).Split('/', '\\').Select(x => x.Trim()).Where(x => x.Length > 0).ToArray(); + + Execute(serverManager => + { + var existing = serverManager.Sites.FirstOrDefault(x => String.Equals(x.Name, webSiteName, StringComparison.OrdinalIgnoreCase)); + if (existing == null) + { + existing = serverManager.Sites.Add(webSiteName, webRootPath, port); + } + + if (virtualParts.Length > 0) + { + var vd = existing.Applications.Single().VirtualDirectories.Add(virtualDirectoryPath, webRootPath); + } + + serverManager.CommitChanges(); + }); + } + + public override string GetHomeDirectory(string webSiteName, string virtualDirectoryPath) + { + string result = null; + + FindVirtualDirectory(webSiteName, virtualDirectoryPath, found => + { + result = found.PhysicalPath; + }); + + if (result == null) + { + throw new Exception("The virtual directory does not exist."); + } + + return result; + } + + public override void DeleteWebSite(string webSiteName) + { + Execute(serverManager => + { + var existing = serverManager.Sites.FirstOrDefault(x => String.Equals(x.Name, webSiteName, StringComparison.OrdinalIgnoreCase)); + if (existing == null) + { + throw new Exception($"The site '{webSiteName}' does not exist."); + } + + existing.Delete(); + serverManager.CommitChanges(); + }); + } + + public void DeleteApplicationPool(string applicationPoolName) + { + Execute(serverManager => + { + var existing = serverManager.ApplicationPools.FirstOrDefault(x => String.Equals(x.Name, applicationPoolName, StringComparison.OrdinalIgnoreCase)); + if (existing == null) + { + throw new Exception($"The application pool '{applicationPoolName}' does not exist"); + } + + existing.Delete(); + serverManager.CommitChanges(); + }); + } + + public override bool ChangeHomeDirectory(string webSiteName, string virtualDirectoryPath, string newWebRootPath) + { + var result = false; + + FindVirtualDirectory(webSiteName, virtualDirectoryPath, found => + { + found.PhysicalPath = newWebRootPath; + result = true; + }); + + return result; + } + + void FindVirtualDirectory(string webSiteName, string virtualDirectoryPath, Action found) + { + Execute(serverManager => + { + var site = serverManager.Sites.FirstOrDefault(s => s.Name.ToLowerInvariant() == webSiteName.ToLowerInvariant()); + if (site == null) + return; + + var virtuals = ListVirtualDirectories("", site); + foreach (var vdir in virtuals) + { + if (!string.Equals(Normalize(vdir.FullVirtualPath), Normalize(virtualDirectoryPath), StringComparison.OrdinalIgnoreCase)) + continue; + + found(vdir.VirtualDirectory); + serverManager.CommitChanges(); + } + }); + } + + static string Normalize(string fullVirtualPath) + { + if (fullVirtualPath == null) + return string.Empty; + + return string.Join("/", fullVirtualPath.Split('/').Where(x => !string.IsNullOrWhiteSpace(x))); + } + + static IEnumerable ListVirtualDirectories(string path, ConfigurationElement element) + { + var site = element as Site; + if (site != null) + { + foreach (var child in site.Applications) + foreach (var item in ListVirtualDirectories("", child)) + yield return item; + } + + var app = element as Application; + if (app != null) + { + foreach (var child in app.VirtualDirectories) + foreach (var item in ListVirtualDirectories(path + "/" + app.Path, child)) + yield return item; + } + + var vdir = element as VirtualDirectory; + if (vdir != null) + { + yield return new VirtualDirectoryNode { FullVirtualPath = path + "/" + vdir.Path, VirtualDirectory = vdir }; + + foreach (var child in vdir.ChildElements) + foreach (var item in ListVirtualDirectories(path + "/" + vdir.Path, child)) + yield return item; + } + } + + public VirtualDirectory FindVirtualDirectory(string webSiteName, string virtualDirectoryPath) + { + VirtualDirectory virtualDirectory = null; + FindVirtualDirectory(webSiteName, virtualDirectoryPath, vd => virtualDirectory = vd); + return virtualDirectory; + } + + public class VirtualDirectoryNode + { + public string FullVirtualPath { get; set; } + public VirtualDirectory VirtualDirectory { get; set; } + } + + public Site GetWebSite(string webSiteName) + { + var site = FindWebSite(webSiteName); + if (site == null) + { + throw new Exception($"The site '{webSiteName}' does not exist."); + } + + return site; + } + + public Site FindWebSite(string webSiteName) + { + return Execute(serverManager => serverManager.Sites.FirstOrDefault(x => String.Equals(x.Name, webSiteName, StringComparison.OrdinalIgnoreCase))); + } + + public bool WebSiteExists(string webSiteName) + { + return FindWebSite(webSiteName) != null; + } + + public ApplicationPool GetApplicationPool(string applicationPoolName) + { + var applicationPool = FindApplicationPool(applicationPoolName); + if (applicationPool == null) + { + throw new Exception($"The application pool '{applicationPoolName}' does not exist."); + } + + return applicationPool; + } + + public ApplicationPool FindApplicationPool(string applicationPoolName) + { + return Execute(serverManager => serverManager.ApplicationPools.FirstOrDefault(x => String.Equals(x.Name, applicationPoolName, StringComparison.OrdinalIgnoreCase))); + } + + public bool ApplicationPoolExists(string applicationPool) + { + return FindApplicationPool(applicationPool) != null; + } + + private void Execute(Action action) + { + using (var serverManager = ServerManager.OpenRemote(Localhost)) + { + action(serverManager); + } + } + + private TResult Execute(Func func) + { + var result = default(TResult); + Action action = serverManager => result = func(serverManager); + Execute(action); + return result; + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Iis/WebServerSixSupport.cs b/source/Calamari.FullFrameworkTools/Iis/WebServerSixSupport.cs new file mode 100644 index 000000000..2d5b7a76c --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Iis/WebServerSixSupport.cs @@ -0,0 +1,202 @@ +using System; +using System.DirectoryServices; +using System.Linq; + +namespace Calamari.FullFrameworkTools.Iis +{ + public class WebServerSixSupport : WebServerSupport + { + public override void CreateWebSiteOrVirtualDirectory(string webSiteName, string virtualDirectoryPath, string webRootPath, int port) + { + var siteId = GetSiteId(webSiteName); + if (siteId == null) + { + using (var w3Svc = new DirectoryEntry("IIS://localhost/w3svc")) + { + siteId = ((int) w3Svc.Invoke("CreateNewSite", new object[] { webSiteName, new object[] { "*:" + port + ":" }, webRootPath })).ToString(); + w3Svc.CommitChanges(); + } + } + + var virtualParts = (virtualDirectoryPath ?? string.Empty).Split('/', '\\').Select(x => x.Trim()).Where(x => x.Length > 0).ToArray(); + + if (virtualParts.Length == 0) + return; + + CreateVirtualDirectory("IIS://localhost/w3svc/" + siteId + "/Root", virtualParts[0], webRootPath, virtualParts.Skip(1).ToArray()); + } + + static void CreateVirtualDirectory(string parentPath, string name, string homeDirectory, string[] remainingPaths) + { + name = name.Trim('/'); + + using (var parent = new DirectoryEntry(parentPath)) + { + string existingChildPath = null; + + foreach (var child in parent.Children.OfType()) + { + if (child.SchemaClassName == "IIsWebVirtualDir" && child.Name.ToLowerInvariant() == name.ToLowerInvariant()) + { + existingChildPath = child.Path; + } + + child.Close(); + } + + if (existingChildPath == null) + { + var child = parent.Children.Add(name.Trim('/'), "IIsWebVirtualDir"); + child.Properties["Path"][0] = homeDirectory; + child.CommitChanges(); + parent.CommitChanges(); + existingChildPath = child.Path; + child.Close(); + } + + if (remainingPaths.Length > 0) + { + CreateVirtualDirectory(existingChildPath, remainingPaths.First(), homeDirectory, remainingPaths.Skip(1).ToArray()); + } + } + } + + public override string GetHomeDirectory(string webSiteName, string virtualDirectoryPath) + { + var siteId = GetSiteId(webSiteName); + if (siteId == null) + { + throw new Exception("The site: " + webSiteName + " does not exist"); + } + + var root = "IIS://localhost/w3svc/" + siteId + "/Root"; + + var virtualParts = (virtualDirectoryPath ?? string.Empty).Split('/', '\\').Select(x => x.Trim()).Where(x => x.Length > 0).ToArray(); + if (virtualParts.Length == 0) + { + using (var entry = new DirectoryEntry(root)) + { + return (string) entry.Properties["Path"][0]; + } + } + + return FindHomeDirectory(root, virtualParts[0], virtualParts.Skip(1).ToArray()); + } + + static string FindHomeDirectory(string parentPath, string virtualDirectoryName, string[] childDirectoryNames) + { + using (var parent = new DirectoryEntry(parentPath)) + { + string existingChildPath = null; + + foreach (var child in parent.Children.OfType()) + { + if (child.SchemaClassName == "IIsWebVirtualDir" && child.Name.ToLowerInvariant() == virtualDirectoryName.ToLowerInvariant()) + { + if (childDirectoryNames.Length == 0) + { + return (string) child.Properties["Path"][0]; + } + + existingChildPath = child.Path; + } + + child.Close(); + } + + if (existingChildPath == null) + { + throw new Exception("The virtual directory: " + virtualDirectoryName + " does not exist"); + } + + return FindHomeDirectory(existingChildPath, childDirectoryNames.First(), childDirectoryNames.Skip(1).ToArray()); + } + } + + public override void DeleteWebSite(string webSiteName) + { + var id = GetSiteId(webSiteName); + if (id == null) + return; + + using (var w3Svc = new DirectoryEntry("IIS://localhost/w3svc")) + { + var child = w3Svc.Children.Find(id, "IIsWebServer"); + child.DeleteTree(); + child.Dispose(); + + w3Svc.CommitChanges(); + } + } + + public override bool ChangeHomeDirectory(string iisWebSiteName, string virtualDirectory, string newWebRootPath) + { + var iisRoot = new DirectoryEntry("IIS://localhost/W3SVC"); + iisRoot.RefreshCache(); + var result = false; + + foreach (DirectoryEntry webSite in iisRoot.Children) + { + if (webSite.SchemaClassName == "IIsWebServer" + && webSite.Properties.Contains("ServerComment") + && (string)webSite.Properties["ServerComment"].Value == iisWebSiteName) + { + foreach (DirectoryEntry webRoot in webSite.Children) + { + if (virtualDirectory == null) + { + if (webRoot.Properties.Contains("Path")) + { + webRoot.Properties["Path"].Value = newWebRootPath; + webRoot.CommitChanges(); + result = true; + } + } + else + { + try + { + var virtualDir = webRoot.Children.Find(virtualDirectory, "IIsWebVirtualDir"); + virtualDir.Properties["Path"].Value = newWebRootPath; + virtualDir.CommitChanges(); + result = true; + } + catch (Exception ex) + { + Console.Error.Write("Unable to find the virtual directory '{0}': {1}", virtualDirectory, ex.Message); + result = false; + } + } + + webRoot.Close(); + } + } + + webSite.Close(); + } + + iisRoot.Close(); + return result; + } + + static string GetSiteId(string webSiteName) + { + string result = null; + + using (var w3Svc = new DirectoryEntry("IIS://localhost/w3svc")) + { + foreach (var child in w3Svc.Children.OfType()) + { + if (child.SchemaClassName == "IIsWebServer" && (string)child.Properties["ServerComment"].Value == webSiteName) + { + result = child.Name; + } + + child.Dispose(); + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Iis/WebServerSupport.cs b/source/Calamari.FullFrameworkTools/Iis/WebServerSupport.cs new file mode 100644 index 000000000..40e9d12de --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Iis/WebServerSupport.cs @@ -0,0 +1,33 @@ +using System; + +namespace Calamari.FullFrameworkTools.Iis +{ + public abstract class WebServerSupport + { + public abstract void CreateWebSiteOrVirtualDirectory(string webSiteName, string virtualDirectoryPath, string webRootPath, int port); + public abstract string GetHomeDirectory(string webSiteName, string virtualDirectoryPath); + public abstract void DeleteWebSite(string webSiteName); + public abstract bool ChangeHomeDirectory(string webSiteName, string virtualDirectoryPath, string newWebRootPath); + + public static WebServerSupport Legacy() + { + return new WebServerSixSupport(); + } + + public static WebServerSupport AutoDetect() + { + // Sources: + // http://support.microsoft.com/kb/224609 + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724832(v=vs.85).aspx + +#pragma warning disable DE0009 // API is deprecated + if (Environment.OSVersion.Version.Major < 6) +#pragma warning restore DE0009 // API is deprecated + { + return new WebServerSixSupport(); + } + + return new WebServerSevenSupport(); + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Program.cs b/source/Calamari.FullFrameworkTools/Program.cs new file mode 100644 index 000000000..534db7fd7 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Program.cs @@ -0,0 +1,79 @@ +using System; +using System.Diagnostics; +using Calamari.FullFrameworkTools.Command; + +namespace Calamari.FullFrameworkTools +{ + public class Program + { + public static int Main(string[] args) + { + var log = new Log(); + if (ExtractArgs(args, out var cmd, out var password, out var file)) + { + WriteHelp(); + return -1; + } + + var commandLocator = new CommandLocator(); + var requestInvoker = new CommandRequestInvoker(commandLocator); + + if (cmd == "version") + { + var fileVersionInfo = FileVersionInfo.GetVersionInfo(typeof(Program).Assembly.Location); + log.Info(fileVersionInfo.ProductVersion); + } + + try + { + var result = requestInvoker.Run(cmd, password, file); + log.Result(result); + } + catch (Exception ex) + { + log.Fatal(ex); + } + return 0; + } + + static void WriteHelp() + { + Console.Error.WriteLine("Commands: iis, win-cert-store, version"); + Console.Error.WriteLine("Usage: --password --file "); + } + + static bool ExtractArgs(string[] args, + out string cmd, + out string password, + out string file) + { + file = ""; + password = ""; + cmd = ""; + + if (args.Length == 0) + { + return false; + } + cmd = args[0]; + + if (args[1] == "--password") + { + password = args[2]; + } else if (args[3] == "--password") + { + password = args[4]; + } + + if (args[1] == "--file") + { + file = args[2]; + } else if (args[3] == "--file") + { + file = args[4]; + } + + return true; + } + } +} \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/Properties/AssemblyInfo.cs b/source/Calamari.FullFrameworkTools/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..0214d90c1 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Calamari.FullFrameworkTools")] +[assembly: InternalsVisibleTo("Calamari.FullFrameworkTools.Tests")] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("D1F10B67-71EF-4024-8F71-178CE48E901F")] diff --git a/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/CertificatePal.cs b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/CertificatePal.cs new file mode 100644 index 000000000..50d208084 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/CertificatePal.cs @@ -0,0 +1,198 @@ +#if WINDOWS_CERTIFICATE_STORE_SUPPORT +using System; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Win32.SafeHandles; +using static Calamari.FullFrameworkTools.WindowsCertStore.WindowsNative.WindowsX509Native; +using Native = Calamari.FullFrameworkTools.WindowsCertStore.WindowsNative.WindowsX509Native; + +namespace Calamari.FullFrameworkTools.WindowsCertStore.WindowsNative +{ + internal static class CertificatePal + { + public static bool HasProperty(IntPtr certificateContext, CertificateProperty property) + { + byte[] buffer = null; + var bufferSize = 0; + // ReSharper disable once ExpressionIsAlwaysNull + var hasProperty = CertGetCertificateContextProperty(certificateContext, property, buffer, ref bufferSize); + + // ReSharper disable once InconsistentNaming + const int ERROR_MORE_DATA = 0x000000ea; + return hasProperty || Marshal.GetLastWin32Error() == ERROR_MORE_DATA; + } + + /// + /// Get a property of a certificate formatted as a structure + /// + public static T GetCertificateProperty(IntPtr certificateContext, CertificateProperty property) where T : struct + { + var rawProperty = GetCertificateProperty(certificateContext, property); + + var gcHandle = GCHandle.Alloc(rawProperty, GCHandleType.Pinned); + var typedProperty = (T)Marshal.PtrToStructure(gcHandle.AddrOfPinnedObject(), typeof(T)); + gcHandle.Free(); + return typedProperty; + } + + public static byte[] GetCertificateProperty(IntPtr certificateContext, CertificateProperty property) + { + byte[] buffer = null; + var bufferSize = 0; + // ReSharper disable once ExpressionIsAlwaysNull + if (!CertGetCertificateContextProperty(certificateContext, property, buffer, ref bufferSize)) + { + // ReSharper disable once InconsistentNaming + const int ERROR_MORE_DATA = 0x000000ea; + var errorCode = Marshal.GetLastWin32Error(); + + if (errorCode != ERROR_MORE_DATA) + { + throw new CryptographicException(errorCode); + } + } + + buffer = new byte[bufferSize]; + if (!CertGetCertificateContextProperty(certificateContext, property, buffer, ref bufferSize)) + { + throw new CryptographicException(Marshal.GetLastWin32Error()); + } + + return buffer; + } + + public static SafeCspHandle GetCspPrivateKey(SafeCertContextHandle certificate) + { + SafeCspHandle cspHandle; + var keySpec = 0; + var freeKey = true; + if (!Native.CryptAcquireCertificatePrivateKey(certificate, + Native.AcquireCertificateKeyOptions.AcquireSilent, + IntPtr.Zero, out cspHandle, out keySpec, out freeKey)) + { + throw new CryptographicException(Marshal.GetLastWin32Error()); + } + + if (cspHandle.IsInvalid) + throw new Exception("Could not acquire private key"); + + if (!freeKey) + { + var addedRef = false; + cspHandle.DangerousAddRef(ref addedRef); + } + + return cspHandle; + } + + public static byte[] GetCspPrivateKeySecurity(SafeCspHandle cspHandle) + { + byte[] buffer = null; + var bufferSize = 0; + + // ReSharper disable once ExpressionIsAlwaysNull + if (!Native.CryptGetProvParam(cspHandle, WindowsX509Native.CspProperties.SecurityDescriptor, buffer, + ref bufferSize, WindowsX509Native.SecurityDesciptorParts.DACL_SECURITY_INFORMATION)) + { + // ReSharper disable once InconsistentNaming + const int ERROR_MORE_DATA = 0x000000ea; + var errorCode = Marshal.GetLastWin32Error(); + + if (errorCode != ERROR_MORE_DATA) + { + throw new CryptographicException(errorCode); + } + } + + buffer = new byte[bufferSize]; + if (!Native.CryptGetProvParam(cspHandle, WindowsX509Native.CspProperties.SecurityDescriptor, buffer, + ref bufferSize, WindowsX509Native.SecurityDesciptorParts.DACL_SECURITY_INFORMATION)) + { + throw new CryptographicException(Marshal.GetLastWin32Error()); + } + + return buffer; + } + + public static byte[] GetCngPrivateKeySecurity(SafeNCryptKeyHandle hObject) + { + int bufferSize = 0; + byte[] buffer = null; + + var errorCode = Native.NCryptGetProperty(hObject, Native.NCryptProperties.SecurityDescriptor, null, 0, + ref bufferSize, + (int)Native.NCryptFlags.Silent | + (int)Native.SecurityDesciptorParts.DACL_SECURITY_INFORMATION); + + if (errorCode != (int)Native.NCryptErrorCode.Success && errorCode != (int)Native.NCryptErrorCode.BufferTooSmall) + { + throw new CryptographicException(errorCode); + } + + buffer = new byte[bufferSize]; + + errorCode = Native.NCryptGetProperty(hObject, Native.NCryptProperties.SecurityDescriptor, buffer, bufferSize, + ref bufferSize, + (int)Native.NCryptFlags.Silent | + (int)Native.SecurityDesciptorParts.DACL_SECURITY_INFORMATION); + + if (errorCode != (int)Native.NCryptErrorCode.Success) + { + throw new CryptographicException(errorCode); + } + + return buffer; + } + + public static SafeNCryptKeyHandle GetCngPrivateKey(SafeCertContextHandle certificate) + { + SafeNCryptKeyHandle key; + int keySpec; + var freeKey = true; + + if (!CryptAcquireCertificatePrivateKey(certificate, + AcquireCertificateKeyOptions.AcquireOnlyNCryptKeys | + AcquireCertificateKeyOptions.AcquireSilent, + IntPtr.Zero, out key, out keySpec, out freeKey)) + { + throw new CryptographicException(Marshal.GetLastWin32Error()); + } + + if (key.IsInvalid) + throw new Exception("Could not acquire provide key"); + + if (!freeKey) + { + var addedRef = false; + key.DangerousAddRef(ref addedRef); + } + + return key; + } + + public static void DeleteCngKey(SafeNCryptKeyHandle key) + { + var errorCode = NCryptDeleteKey(key, 0); + + if (errorCode != 0) + throw new CryptographicException(errorCode); + } + + public static string GetSubjectName(SafeCertContextHandle certificate) + { + var flags = CertNameFlags.None; + var stringType = CertNameStringType.CERT_X500_NAME_STR | CertNameStringType.CERT_NAME_STR_REVERSE_FLAG; + + var cchCount = CertGetNameString(certificate, CertNameType.CERT_NAME_RDN_TYPE, flags, ref stringType, null, 0); + if (cchCount == 0) + throw new CryptographicException(Marshal.GetHRForLastWin32Error()); + + var sb = new StringBuilder(cchCount); + CertGetNameString(certificate, CertNameType.CERT_NAME_RDN_TYPE, flags, ref stringType, sb, cchCount); + + return sb.ToString(); + } + } +} +#endif \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCertContextHandle.cs b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCertContextHandle.cs new file mode 100644 index 000000000..332078c50 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCertContextHandle.cs @@ -0,0 +1,61 @@ +#if WINDOWS_CERTIFICATE_STORE_SUPPORT +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Calamari.FullFrameworkTools.WindowsCertStore.WindowsNative +{ + /// + /// + /// SafeCertContextHandle provides a SafeHandle class for an X509Certificate's certificate context + /// as stored in its + /// property. This can be used instead of the raw IntPtr to avoid races with the garbage + /// collector, ensuring that the X509Certificate object is not cleaned up from underneath you + /// while you are still using the handle pointer. + /// + /// + /// This safe handle type represents a native CERT_CONTEXT. + /// (http://msdn.microsoft.com/en-us/library/aa377189.aspx) + /// + /// + /// A SafeCertificateContextHandle for an X509Certificate can be obtained by calling the extension method. + /// + /// + internal sealed class SafeCertContextHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private SafeCertContextHandle() : base(true) + { + } + + public SafeCertContextHandle(IntPtr handle, bool ownsHandle) + : base(false) + { + SetHandle(handle); + } + + [DllImport("crypt32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CertFreeCertificateContext(IntPtr pCertContext); + + public WindowsX509Native.CERT_CONTEXT CertificateContext => (WindowsX509Native.CERT_CONTEXT)Marshal.PtrToStructure(handle, typeof(WindowsX509Native.CERT_CONTEXT)); + + protected override bool ReleaseHandle() + { + return CertFreeCertificateContext(handle); + } + + public SafeCertContextHandle Duplicate() + { + return WindowsX509Native.CertDuplicateCertificateContext(this.DangerousGetHandle()); + } + + public IntPtr Disconnect() + { + var ptr = DangerousGetHandle(); + SetHandle(IntPtr.Zero); + return ptr; + } + } +} +#endif \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCertContextHandleExtensions.cs b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCertContextHandleExtensions.cs new file mode 100644 index 000000000..90c80686b --- /dev/null +++ b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCertContextHandleExtensions.cs @@ -0,0 +1,30 @@ +using System; + +#if WINDOWS_CERTIFICATE_STORE_SUPPORT + +namespace Calamari.FullFrameworkTools.WindowsCertStore.WindowsNative +{ + internal static class SafeCertContextHandleExtensions + { + public static bool HasPrivateKey(this SafeCertContextHandle certificateContext) + { + return certificateContext.HasProperty(WindowsX509Native.CertificateProperty.KeyProviderInfo); + } + + public static bool HasProperty(this SafeCertContextHandle certificateContext, + WindowsX509Native.CertificateProperty property) + { + return CertificatePal.HasProperty(certificateContext.DangerousGetHandle(), property); + } + + /// + /// Get a property of a certificate formatted as a structure + /// + public static T GetCertificateProperty(this SafeCertContextHandle certificateContext, + WindowsX509Native.CertificateProperty property) where T : struct + { + return CertificatePal.GetCertificateProperty(certificateContext.DangerousGetHandle(), property); + } + } +} +#endif \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCertStoreHandle.cs b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCertStoreHandle.cs new file mode 100644 index 000000000..1c2feffcf --- /dev/null +++ b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCertStoreHandle.cs @@ -0,0 +1,32 @@ +#if WINDOWS_CERTIFICATE_STORE_SUPPORT +using System; +using Microsoft.Win32.SafeHandles; + +namespace Calamari.FullFrameworkTools.WindowsCertStore.WindowsNative +{ + internal class SafeCertStoreHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private SafeCertStoreHandle() : base(true) + { + } + + private SafeCertStoreHandle(IntPtr handle) + : this(handle, true) + { + } + + public SafeCertStoreHandle(IntPtr handle, bool ownsHandle) + : base(ownsHandle) + { + SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + return WindowsX509Native.CertCloseStore(base.handle, 0); + } + + public static SafeCertStoreHandle InvalidHandle => new SafeCertStoreHandle(IntPtr.Zero); + } +} +#endif diff --git a/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCspHandle.cs b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCspHandle.cs new file mode 100644 index 000000000..e43b637c1 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/SafeCspHandle.cs @@ -0,0 +1,66 @@ +#if WINDOWS_CERTIFICATE_STORE_SUPPORT +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using Microsoft.Win32.SafeHandles; + +namespace Calamari.FullFrameworkTools.WindowsCertStore.WindowsNative +{ + internal class SafeCspHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private SafeCspHandle() + : base(true) + { + } + + [DllImport("advapi32", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CryptContextAddRef(SafeCspHandle hProv, IntPtr pdwReserved, int dwFlags); + + [DllImport("advapi32")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CryptReleaseContext(IntPtr hProv, int dwFlags); + + public SafeCspHandle Duplicate() + { + bool success = false; + RuntimeHelpers.PrepareConstrainedRegions(); + try + { + this.DangerousAddRef(ref success); + IntPtr handle = this.DangerousGetHandle(); + int hr = 0; + SafeCspHandle safeCspHandle = new SafeCspHandle(); + RuntimeHelpers.PrepareConstrainedRegions(); + try + { + } + finally + { + if (!SafeCspHandle.CryptContextAddRef(this, IntPtr.Zero, 0)) + hr = Marshal.GetLastWin32Error(); + else + safeCspHandle.SetHandle(handle); + } + if (hr != 0) + { + safeCspHandle.Dispose(); + throw new CryptographicException(hr); + } + return safeCspHandle; + } + finally + { + if (success) + this.DangerousRelease(); + } + } + + protected override bool ReleaseHandle() + { + return SafeCspHandle.CryptReleaseContext(this.handle, 0); + } + } +} +#endif \ No newline at end of file diff --git a/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/WindowsX509Native.cs b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/WindowsX509Native.cs new file mode 100644 index 000000000..e8d4944b4 --- /dev/null +++ b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsNative/WindowsX509Native.cs @@ -0,0 +1,336 @@ +#if WINDOWS_CERTIFICATE_STORE_SUPPORT +using System; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Win32.SafeHandles; + +namespace Calamari.FullFrameworkTools.WindowsCertStore.WindowsNative +{ + internal static class WindowsX509Native + { + [DllImport("Crypt32.dll", SetLastError = true)] + public static extern SafeCertStoreHandle CertOpenStore(CertStoreProviders lpszStoreProvider, IntPtr notUsed, + IntPtr notUsed2, CertificateSystemStoreLocation location, [MarshalAs(UnmanagedType.LPWStr)] string storeName); + + [DllImport("Crypt32.dll", SetLastError = true)] + public static extern bool CertCloseStore(IntPtr hCertStore, int dwFlags); + + [DllImport("Crypt32.dll", SetLastError = true)] + public static extern SafeCertStoreHandle PFXImportCertStore(ref CryptoData pPfx, + [MarshalAs(UnmanagedType.LPWStr)] string szPassword, PfxImportFlags dwFlags); + + [DllImport("Crypt32.dll", SetLastError = true)] + public static extern bool CertAddCertificateContextToStore(SafeCertStoreHandle hCertStore, + SafeCertContextHandle pCertContext, AddCertificateDisposition dwAddDisposition, ref IntPtr ppStoreContext); + + [DllImport("Crypt32.dll", SetLastError = true)] + public static extern SafeCertContextHandle CertFindCertificateInStore(SafeCertStoreHandle hCertStore, + CertificateEncodingType dwCertEncodingType, IntPtr notUsed, CertificateFindType dwFindType, + ref CryptoData pvFindPara, IntPtr pPrevCertContext); + + [DllImport("Crypt32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern SafeCertContextHandle CertDuplicateCertificateContext(IntPtr pCertContext); + + [DllImport("Crypt32.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "CertGetNameStringW")] + public static extern int CertGetNameString(SafeCertContextHandle pCertContext, CertNameType dwType, CertNameFlags dwFlags, [In] ref CertNameStringType pvPara, [Out] StringBuilder pszNameString, int cchNameString); + + [DllImport("Crypt32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CertCompareCertificateName(CertificateEncodingType dwCertEncodingType, + ref CryptoData pCertName1, ref CryptoData pCertName2); + + [DllImport("Crypt32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CertGetCertificateContextProperty(IntPtr pCertContext, CertificateProperty dwPropId, + [Out, MarshalAs(UnmanagedType.LPArray)] byte[] pvData, [In, Out] ref int pcbData); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CryptAcquireContextW")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CryptAcquireContext(out IntPtr psafeProvHandle, + [MarshalAs(UnmanagedType.LPWStr)] string pszContainer, + [MarshalAs(UnmanagedType.LPWStr)] string pszProvider, + int dwProvType, CryptAcquireContextFlags dwFlags); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CryptGetProvParam(SafeCspHandle hProv, CspProperties dwParam, [Out, MarshalAs(UnmanagedType.LPArray)] byte[] pbData, ref int pdwDataLen, SecurityDesciptorParts dwFlags); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CryptSetProvParam(SafeCspHandle hProv, CspProperties dwParam, [In] byte[] pbData, SecurityDesciptorParts dwFlags); + + [DllImport("Crypt32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CryptAcquireCertificatePrivateKey(SafeCertContextHandle pCert, + AcquireCertificateKeyOptions dwFlags, + IntPtr pvReserved, // void * + [Out] out SafeCspHandle phCryptProvOrNCryptKey, + [Out] out int dwKeySpec, + [Out, MarshalAs(UnmanagedType.Bool)] out bool pfCallerFreeProvOrNCryptKey); + + [DllImport("Crypt32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CryptAcquireCertificatePrivateKey(SafeCertContextHandle pCert, + AcquireCertificateKeyOptions dwFlags, + IntPtr pvReserved, // void * + [Out] out SafeNCryptKeyHandle phCryptProvOrNCryptKey, + [Out] out int dwKeySpec, + [Out, MarshalAs(UnmanagedType.Bool)] out bool pfCallerFreeProvOrNCryptKey); + + [DllImport("Crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern + bool CertEnumSystemStore(CertificateSystemStoreLocation dwFlags, IntPtr notUsed1, IntPtr notUsed2, + CertEnumSystemStoreCallBackProto fn); + + /// + /// signature of call back function used by CertEnumSystemStore + /// + internal delegate + bool CertEnumSystemStoreCallBackProto( + [MarshalAs(UnmanagedType.LPWStr)] string storeName, uint dwFlagsNotUsed, IntPtr notUsed1, + IntPtr notUsed2, IntPtr notUsed3); + + [DllImport("Ncrypt.dll", SetLastError = true, ExactSpelling = true)] + internal static extern int NCryptGetProperty(SafeNCryptHandle hObject, [MarshalAs(UnmanagedType.LPWStr)] string szProperty, [Out, MarshalAs(UnmanagedType.LPArray)] byte[] pbOutput, int cbOutput, ref int pcbResult, int flags); + + [DllImport("Ncrypt.dll", SetLastError = true, ExactSpelling = true)] + internal static extern int NCryptSetProperty(SafeNCryptHandle hObject, [MarshalAs(UnmanagedType.LPWStr)] string szProperty, IntPtr pbInputByteArray, int cbInput, int flags); + + [DllImport("Ncrypt.dll")] + internal static extern int NCryptDeleteKey(SafeNCryptKeyHandle hKey, int flags); + + [Flags] + internal enum CertStoreProviders + { + CERT_STORE_PROV_SYSTEM = 10 + } + + internal enum AddCertificateDisposition + { + CERT_STORE_ADD_NEW = 1, + CERT_STORE_ADD_REPLACE_EXISTING = 3 + } + + internal enum CertificateSystemStoreLocation + { + CurrentUser = 1 << 16, // CERT_SYSTEM_STORE_CURRENT_USER + LocalMachine = 2 << 16, // CERT_SYSTEM_STORE_LOCAL_MACHINE + CurrentService = 4 << 16, // CERT_SYSTEM_STORE_CURRENT_SERVICE + Services = 5 << 16, // CERT_SYSTEM_STORE_SERVICES + Users = 6 << 16, // CERT_SYSTEM_STORE_USERS + UserGroupPolicy = 7 << 16, // CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY + MachineGroupPolicy = 8 << 16, // CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY + LocalMachineEnterprise = 9 << 16, // CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE + } + + internal enum CertificateFindType + { + Sha1Hash = 1 << 16 // CERT_FIND_SHA1_HASH + } + + [Flags] + internal enum CertificateEncodingType + { + X509AsnEncoding = 0x00000001, // X509_ASN_ENCODING + Pkcs7AsnEncoding = 0x00010000, // PKCS_7_ASN_ENCODING + Pkcs7OrX509AsnEncoding = X509AsnEncoding | Pkcs7AsnEncoding + } + + internal enum CertNameType + { + CERT_NAME_EMAIL_TYPE = 1, + CERT_NAME_RDN_TYPE = 2, + CERT_NAME_ATTR_TYPE = 3, + CERT_NAME_SIMPLE_DISPLAY_TYPE = 4, + CERT_NAME_FRIENDLY_DISPLAY_TYPE = 5, + CERT_NAME_DNS_TYPE = 6, + CERT_NAME_URL_TYPE = 7, + CERT_NAME_UPN_TYPE = 8, + } + + [Flags] + internal enum CertNameFlags + { + None = 0x00000000, + CERT_NAME_ISSUER_FLAG = 0x00000001, + } + + [Flags] + internal enum CertNameStringType + { + CERT_X500_NAME_STR = 3, + CERT_NAME_STR_REVERSE_FLAG = 0x02000000, + } + + // CRYPTOAPI_BLOB + [StructLayout(LayoutKind.Sequential)] + public struct CryptoData + { + public int cbData; + public IntPtr pbData; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct KeyProviderInfo + { + [MarshalAs(UnmanagedType.LPWStr)] internal string pwszContainerName; + + [MarshalAs(UnmanagedType.LPWStr)] internal string pwszProvName; + + internal int dwProvType; + + internal int dwFlags; + + internal int cProvParam; + + internal IntPtr rgProvParam; // PCRYPT_KEY_PROV_PARAM + + internal int dwKeySpec; + } + + [Flags] + public enum PfxImportFlags + { + CRYPT_EXPORTABLE = 0x00000001, + CRYPT_MACHINE_KEYSET = 0x00000020, + CRYPT_USER_KEYSET = 0x00001000, + PKCS12_PREFER_CNG_KSP = 0x00000100, + PKCS12_ALWAYS_CNG_KSP = 0x00000200 + } + + /// + /// Well known certificate property IDs + /// + public enum CertificateProperty + { + KeyProviderInfo = 2, // CERT_KEY_PROV_INFO_PROP_ID + KeyContext = 5, // CERT_KEY_CONTEXT_PROP_ID + } + + /// + /// Flags for the CryptAcquireCertificatePrivateKey API + /// + [Flags] + internal enum AcquireCertificateKeyOptions + { + None = 0x00000000, + AcquireSilent = 0x00000040, + AcquireAllowNCryptKeys = 0x00010000, // CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG + AcquireOnlyNCryptKeys = 0x00040000, // CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG + } + + [Flags] + internal enum CryptAcquireContextFlags + { + None = 0x00000000, + Delete = 0x00000010, // CRYPT_DELETEKEYSET + MachineKeySet = 0x00000020, // CRYPT_MACHINE_KEYSET + Silent = 0x40 // CRYPT_SILENT + } + + public enum CspProperties + { + SecurityDescriptor = 0x8 // PP_KEYSET_SEC_DESCR + } + + public static class NCryptProperties + { + public const string SecurityDescriptor = "Security Descr"; // NCRYPT_SECURITY_DESCR_PROPERTY + } + + [Flags] + public enum NCryptFlags + { + Silent = 0x00000040, + } + + public enum SecurityDesciptorParts + { + DACL_SECURITY_INFORMATION = 0x00000004 + } + + public enum NCryptErrorCode + { + Success = 0x00000000, // ERROR_SUCCESS + BufferTooSmall = unchecked((int) 0x80090028), // NTE_BUFFER_TOO_SMALL + } + + [StructLayout(LayoutKind.Sequential)] + internal struct CERT_CONTEXT + { + public CertificateEncodingType dwCertEncodingType; + public IntPtr pbCertEncoded; + public int cbCertEncoded; + public IntPtr pCertInfo; + public IntPtr hCertStore; + } + + public enum CapiErrorCode + { + CRYPT_E_EXISTS = unchecked((int) 0x80092005) + } + + [StructLayout(LayoutKind.Sequential)] + internal struct CERT_INFO + { + public int dwVersion; + public CryptoData SerialNumber; + public CRYPT_ALGORITHM_IDENTIFIER SignatureAlgorithm; + public CryptoData Issuer; + public FILETIME NotBefore; + public FILETIME NotAfter; + public CryptoData Subject; + public CERT_PUBLIC_KEY_INFO SubjectPublicKeyInfo; + public CRYPT_BIT_BLOB IssuerUniqueId; + public CRYPT_BIT_BLOB SubjectUniqueId; + public int cExtension; + public IntPtr rgExtension; +} + + [StructLayout(LayoutKind.Sequential)] + internal struct FILETIME + { + private uint ftTimeLow; + private uint ftTimeHigh; + + public DateTime ToDateTime() + { + long fileTime = (((long) ftTimeHigh) << 32) + ftTimeLow; + return DateTime.FromFileTime(fileTime); + } + + public static FILETIME FromDateTime(DateTime dt) + { + long fileTime = dt.ToFileTime(); + return new FILETIME() + { + ftTimeLow = (uint) fileTime, + ftTimeHigh = (uint) (fileTime >> 32), + }; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct CERT_PUBLIC_KEY_INFO + { + public CRYPT_ALGORITHM_IDENTIFIER Algorithm; + public CRYPT_BIT_BLOB PublicKey; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct CRYPT_ALGORITHM_IDENTIFIER + { + public IntPtr pszObjId; + public CryptoData Parameters; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct CRYPT_BIT_BLOB + { + public int cbData; + public IntPtr pbData; + public int cUnusedBits; + } + } +} +#endif \ No newline at end of file diff --git a/source/Calamari.Shared/Integration/Certificates/WindowsX509CertificateStore.cs b/source/Calamari.Shared/Integration/Certificates/WindowsX509CertificateStore.cs index b7a6851a2..21ed4d63a 100644 --- a/source/Calamari.Shared/Integration/Certificates/WindowsX509CertificateStore.cs +++ b/source/Calamari.Shared/Integration/Certificates/WindowsX509CertificateStore.cs @@ -165,6 +165,7 @@ public static CryptoKeySecurity GetPrivateKeySecurity(string thumbprint, StoreLo } } + // Used in IIS Tests /// /// Unlike X509Store.Remove() this function also cleans up private-keys /// diff --git a/source/Calamari.Tests/Fixtures/Iis/IisFixture.cs b/source/Calamari.Tests/Fixtures/Iis/IisFixture.cs index d8f4ab06f..275851bda 100644 --- a/source/Calamari.Tests/Fixtures/Iis/IisFixture.cs +++ b/source/Calamari.Tests/Fixtures/Iis/IisFixture.cs @@ -6,6 +6,11 @@ namespace Calamari.Tests.Fixtures.Iis { + /// + /// Note. This test is a direct clone of . + /// While we extract IIS functionality out of Calamari, we will have the relevant functionality in both places for a short period. + /// Ensure any changes to make to this test are reflected in the clone test. + /// [TestFixture] [Category(TestCategory.CompatibleOS.OnlyWindows)] public class IisFixture diff --git a/source/Calamari.sln b/source/Calamari.sln index 8d1fdc89f..8ca19dbb5 100644 --- a/source/Calamari.sln +++ b/source/Calamari.sln @@ -82,6 +82,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calamari.Scripting", "Calam EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calamari.Scripting.Tests", "Calamari.Scripting.Tests\Calamari.Scripting.Tests.csproj", "{D8DEC40C-948F-4806-AE87-1A7502E41A06}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calamari.FullFrameworkTools", "Calamari.FullFrameworkTools\Calamari.FullFrameworkTools.csproj", "{D1F10B67-71EF-4024-8F71-178CE48E901F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calamari.FullFrameworkTools.Tests", "Calamari.FullFrameworkTools.Tests\Calamari.FullFrameworkTools.Tests.csproj", "{EC26DBFD-A364-4AF9-BFEF-F8ABCA2656FA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -206,6 +210,14 @@ Global {D8DEC40C-948F-4806-AE87-1A7502E41A06}.Debug|Any CPU.Build.0 = Debug|Any CPU {D8DEC40C-948F-4806-AE87-1A7502E41A06}.Release|Any CPU.ActiveCfg = Release|Any CPU {D8DEC40C-948F-4806-AE87-1A7502E41A06}.Release|Any CPU.Build.0 = Release|Any CPU + {D1F10B67-71EF-4024-8F71-178CE48E901F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1F10B67-71EF-4024-8F71-178CE48E901F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1F10B67-71EF-4024-8F71-178CE48E901F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1F10B67-71EF-4024-8F71-178CE48E901F}.Release|Any CPU.Build.0 = Release|Any CPU + {EC26DBFD-A364-4AF9-BFEF-F8ABCA2656FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC26DBFD-A364-4AF9-BFEF-F8ABCA2656FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC26DBFD-A364-4AF9-BFEF-F8ABCA2656FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC26DBFD-A364-4AF9-BFEF-F8ABCA2656FA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/source/Calamari/Calamari.csproj b/source/Calamari/Calamari.csproj index eca113155..ccef9198a 100644 --- a/source/Calamari/Calamari.csproj +++ b/source/Calamari/Calamari.csproj @@ -72,6 +72,7 @@ + diff --git a/source/Calamari/Commands/DeployPackageCommand.cs b/source/Calamari/Commands/DeployPackageCommand.cs index e5b161244..c27cb7ecd 100644 --- a/source/Calamari/Commands/DeployPackageCommand.cs +++ b/source/Calamari/Commands/DeployPackageCommand.cs @@ -15,6 +15,7 @@ using Calamari.Common.Features.Scripting; using Calamari.Common.Features.StructuredVariables; using Calamari.Common.Features.Substitutions; +using Calamari.Common.FeatureToggles; using Calamari.Common.Plumbing; using Calamari.Common.Plumbing.Deployment; using Calamari.Common.Plumbing.Deployment.Journal; @@ -86,7 +87,9 @@ public override int Execute(string[] commandLineArguments) var transformFileLocator = new TransformFileLocator(fileSystem, log); var embeddedResources = new AssemblyEmbeddedResources(); #if IIS_SUPPORT - var iis = new InternetInformationServer(); + var iis = OctopusFeatureToggles.FullFrameworkTasksExternalProcess.IsEnabled(variables) ? + (IInternetInformationServer)new InProcessFullFrameworkToolInternetInformationServer() : + new InternetInformationServer(); featureClasses.AddRange(new IFeature[] { new IisWebSiteBeforeDeployFeature(), new IisWebSiteAfterPostDeployFeature() }); #endif if (!CalamariEnvironment.IsRunningOnWindows) diff --git a/source/Calamari/Integration/Iis/InProcessFullFrameworkToolInternetInformationServer.cs b/source/Calamari/Integration/Iis/InProcessFullFrameworkToolInternetInformationServer.cs new file mode 100644 index 000000000..888d387ec --- /dev/null +++ b/source/Calamari/Integration/Iis/InProcessFullFrameworkToolInternetInformationServer.cs @@ -0,0 +1,23 @@ +#if IIS_SUPPORT +using System; +using Calamari.FullFrameworkTools.Command; + +namespace Calamari.Integration.Iis +{ + public class InProcessFullFrameworkToolInternetInformationServer: IInternetInformationServer + { + public bool OverwriteHomeDirectory(string iisWebSiteName, string path, bool legacySupport) + { + var commandLocator = new CommandLocator(); + var cmd = commandLocator.GetCommand(); + var result = (OverwriteHomeDirectoryResponse)cmd.Handle(new OverwriteHomeDirectoryRequest() + { + Path = path, + IisWebSiteName = iisWebSiteName, + LegacySupport = legacySupport + }); + return result.Result; + } + } +} +#endif \ No newline at end of file diff --git a/source/Calamari/Integration/Iis/InternetInformationServer.cs b/source/Calamari/Integration/Iis/InternetInformationServer.cs index 62fdd83d2..15aa2c88e 100644 --- a/source/Calamari/Integration/Iis/InternetInformationServer.cs +++ b/source/Calamari/Integration/Iis/InternetInformationServer.cs @@ -1,4 +1,5 @@ #if IIS_SUPPORT +using System; using System.Linq; namespace Calamari.Integration.Iis @@ -8,7 +9,6 @@ namespace Calamari.Integration.Iis /// public class InternetInformationServer : IInternetInformationServer { - /// /// Sets the home directory (web root) of the given IIS website to the given path. /// @@ -31,6 +31,5 @@ public bool OverwriteHomeDirectory(string iisWebSiteNameAndVirtualDirectory, str return server.ChangeHomeDirectory(iisSiteName, virtualDirectory, path); } - } -} + } } #endif \ No newline at end of file From 0dd875c2da1dea1045e4e954eee7a2d1c5c03b87 Mon Sep 17 00:00:00 2001 From: Robert E Date: Fri, 23 Aug 2024 20:49:07 +1000 Subject: [PATCH 3/8] Proxy request example --- .../Calamari.FullFrameworkTools.Tests.csproj | 1 + .../Command/CommandRequestInvokerTests.cs | 85 ++- .../SerializedLogTest.cs | 18 + .../Calamari.FullFrameworkTools.csproj | 10 +- .../Command/CommandHandler.cs | 41 ++ .../Command/CommandLocator.cs | 5 +- .../Command/CommandRequestInvoker.cs | 44 +- .../Command/ICommand.cs | 31 - .../Command/IRequest.cs | 8 + .../ImportCertificateToStoreHandler.cs | 29 - .../Command/{Log.cs => SerializedLog.cs} | 23 +- .../OverwriteHomeDirectoryHandler.cs | 8 +- source/Calamari.FullFrameworkTools/Program.cs | 14 +- .../{Command => Utils}/AesEncryption.cs | 2 +- .../{Command => Utils}/ILog.cs | 0 .../IWindowsX509CertificateStore.cs | 154 +++++ .../WindowsCertStore/PrivateKeyAccessRule.cs | 35 ++ .../SystemSemaphoreManager.cs | 150 +++++ .../WindowsNative/WindowsX509Native.cs | 11 +- .../WindowsX509CertificateStore.cs | 574 ++++++++++++++++++ .../Deployment/DeployPackageFixture.cs | 2 +- .../Logger/AbstractLogFixture.cs | 6 +- source/Calamari/Calamari.csproj | 6 + .../Calamari/Commands/DeployPackageCommand.cs | 2 +- ...lFrameworkToolInternetInformationServer.cs | 23 - .../Integration/Iis/LegacyFrameworkInvoker.cs | 133 ++++ .../Iis/LegacyInternetInformationServer.cs | 108 ++++ source/Calamari/Program.cs | 2 +- 28 files changed, 1392 insertions(+), 133 deletions(-) create mode 100644 source/Calamari.FullFrameworkTools.Tests/SerializedLogTest.cs create mode 100644 source/Calamari.FullFrameworkTools/Command/CommandHandler.cs delete mode 100644 source/Calamari.FullFrameworkTools/Command/ICommand.cs create mode 100644 source/Calamari.FullFrameworkTools/Command/IRequest.cs delete mode 100644 source/Calamari.FullFrameworkTools/Command/ImportCertificateToStoreHandler.cs rename source/Calamari.FullFrameworkTools/Command/{Log.cs => SerializedLog.cs} (53%) rename source/Calamari.FullFrameworkTools/{Command => Iis}/OverwriteHomeDirectoryHandler.cs (90%) rename source/Calamari.FullFrameworkTools/{Command => Utils}/AesEncryption.cs (98%) rename source/Calamari.FullFrameworkTools/{Command => Utils}/ILog.cs (100%) create mode 100644 source/Calamari.FullFrameworkTools/WindowsCertStore/IWindowsX509CertificateStore.cs create mode 100644 source/Calamari.FullFrameworkTools/WindowsCertStore/PrivateKeyAccessRule.cs create mode 100644 source/Calamari.FullFrameworkTools/WindowsCertStore/SystemSemaphoreManager.cs create mode 100644 source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsX509CertificateStore.cs delete mode 100644 source/Calamari/Integration/Iis/InProcessFullFrameworkToolInternetInformationServer.cs create mode 100644 source/Calamari/Integration/Iis/LegacyFrameworkInvoker.cs create mode 100644 source/Calamari/Integration/Iis/LegacyInternetInformationServer.cs diff --git a/source/Calamari.FullFrameworkTools.Tests/Calamari.FullFrameworkTools.Tests.csproj b/source/Calamari.FullFrameworkTools.Tests/Calamari.FullFrameworkTools.Tests.csproj index 6872b8775..3299d5a3a 100644 --- a/source/Calamari.FullFrameworkTools.Tests/Calamari.FullFrameworkTools.Tests.csproj +++ b/source/Calamari.FullFrameworkTools.Tests/Calamari.FullFrameworkTools.Tests.csproj @@ -11,6 +11,7 @@ net462 Calamari.FullFrameworkTools.Tests + + + + + + + + + + + true + false + + + - + + + + diff --git a/source/Calamari/Commands/DeployPackageCommand.cs b/source/Calamari/Commands/DeployPackageCommand.cs index f8b31a76d..898418060 100644 --- a/source/Calamari/Commands/DeployPackageCommand.cs +++ b/source/Calamari/Commands/DeployPackageCommand.cs @@ -26,6 +26,7 @@ using Calamari.Deployment.Conventions; using Calamari.Deployment.Features; using Calamari.Integration.Certificates; +using Calamari.Integration.FullFramework; using Calamari.Integration.Iis; using Calamari.Integration.Nginx; @@ -90,8 +91,8 @@ public override int Execute(string[] commandLineArguments) var transformFileLocator = new TransformFileLocator(fileSystem, log); var embeddedResources = new AssemblyEmbeddedResources(); #if IIS_SUPPORT - var iis = OctopusFeatureToggles.FullFrameworkTasksExternalProcess.IsEnabled(variables) ? - new InternetInformationServer(): (IInternetInformationServer)new LegacyInternetInformationServer(new InProcessInvoker()); + var iis = !OctopusFeatureToggles.FullFrameworkTasksExternalProcess.IsEnabled(variables) ? + new InternetInformationServer(): (IInternetInformationServer)new LegacyInternetInformationServer(new LegacyFrameworkInvoker()); featureClasses.AddRange(new IFeature[] { new IisWebSiteBeforeDeployFeature(windowsX509CertificateStore), new IisWebSiteAfterPostDeployFeature(windowsX509CertificateStore) }); #endif if (!CalamariEnvironment.IsRunningOnWindows) diff --git a/source/Calamari/Integration/FullFramework/AddPrivateKeyAccessRulesRequest.cs b/source/Calamari/Integration/FullFramework/AddPrivateKeyAccessRulesRequest.cs new file mode 100644 index 000000000..942d2c070 --- /dev/null +++ b/source/Calamari/Integration/FullFramework/AddPrivateKeyAccessRulesRequest.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using Calamari.Integration.Certificates; + +namespace Calamari.Integration.FullFramework +{ + + public interface IRequest + { + } + + public class AddPrivateKeyAccessRulesRequest : IRequest + { + public AddPrivateKeyAccessRulesRequest(string thumbprint, StoreLocation storeLocation, string storeName, List privateKeyAccessRules) + { + this.Thumbprint = thumbprint; + StoreName = storeName; + PrivateKeyAccessRules = privateKeyAccessRules; + StoreLocation = storeLocation; + } + + public string Thumbprint { get; set; } + public StoreLocation StoreLocation { get; set; } + public string StoreName { get; set; } + public List PrivateKeyAccessRules { get; set; } + } +} \ No newline at end of file diff --git a/source/Calamari/Integration/FullFramework/FindCertificateStoreRequest.cs b/source/Calamari/Integration/FullFramework/FindCertificateStoreRequest.cs new file mode 100644 index 000000000..1433f3faf --- /dev/null +++ b/source/Calamari/Integration/FullFramework/FindCertificateStoreRequest.cs @@ -0,0 +1,18 @@ +using System; +using System.Security.Cryptography.X509Certificates; + +namespace Calamari.Integration.FullFramework +{ + + public class FindCertificateStoreRequest : IRequest + { + public FindCertificateStoreRequest(string thumbprint, StoreLocation storeLocation) + { + Thumbprint = thumbprint; + StoreLocation = storeLocation; + } + + public string Thumbprint { get; } + public StoreLocation StoreLocation { get; } + } +} \ No newline at end of file diff --git a/source/Calamari/Integration/FullFramework/ImportCertificateToStoreByLocationRequest.cs b/source/Calamari/Integration/FullFramework/ImportCertificateToStoreByLocationRequest.cs new file mode 100644 index 000000000..2ece0b4ee --- /dev/null +++ b/source/Calamari/Integration/FullFramework/ImportCertificateToStoreByLocationRequest.cs @@ -0,0 +1,24 @@ +using System; +using System.Security.Cryptography.X509Certificates; + +namespace Calamari.Integration.FullFramework { + +public class ImportCertificateToStoreByLocationRequest : IRequest +{ + public ImportCertificateToStoreByLocationRequest(byte[] pfxBytes, string password, StoreLocation storeLocation,string storeName, bool privateKeyExportable) + { + PfxBytes = pfxBytes; + Password = password; + StoreLocation = storeLocation; + StoreName = storeName; + PrivateKeyExportable = privateKeyExportable; + } + + public byte[] PfxBytes { get; set; } + public string Password { get; set; } + public StoreLocation StoreLocation { get; set; } + public string StoreName { get; set; } + public bool PrivateKeyExportable { get; set; } + +} +} \ No newline at end of file diff --git a/source/Calamari/Integration/FullFramework/ImportCertificateToStoreByUserRequest.cs b/source/Calamari/Integration/FullFramework/ImportCertificateToStoreByUserRequest.cs new file mode 100644 index 000000000..5dadf2c39 --- /dev/null +++ b/source/Calamari/Integration/FullFramework/ImportCertificateToStoreByUserRequest.cs @@ -0,0 +1,28 @@ +using System; + +namespace Calamari.Integration.FullFramework +{ + + public class ImportCertificateToStoreByUserRequest : IRequest + { + public ImportCertificateToStoreByUserRequest(byte[] pfxBytes, + string password, + string userName, + string storeName, + bool privateKeyExportable) + { + PfxBytes = pfxBytes; + Password = password; + UserName = userName; + StoreName = storeName; + PrivateKeyExportable = privateKeyExportable; + } + + public byte[] PfxBytes { get; set; } + public string Password { get; set; } + public string UserName { get; set; } + public string StoreName { get; set; } + public bool PrivateKeyExportable { get; set; } + + } +} \ No newline at end of file diff --git a/source/Calamari/Integration/Iis/LegacyFrameworkInvoker.cs b/source/Calamari/Integration/FullFramework/LegacyFrameworkInvoker.cs similarity index 81% rename from source/Calamari/Integration/Iis/LegacyFrameworkInvoker.cs rename to source/Calamari/Integration/FullFramework/LegacyFrameworkInvoker.cs index cea7e4bd3..4b04ebe8c 100644 --- a/source/Calamari/Integration/Iis/LegacyFrameworkInvoker.cs +++ b/source/Calamari/Integration/FullFramework/LegacyFrameworkInvoker.cs @@ -1,24 +1,16 @@ -#if IIS_SUPPORT -using System; -using System.Collections.Generic; +using System; using System.IO; -using System.Security.Cryptography.X509Certificates; +using System.Reflection; using Calamari.Common.Features.Processes; +using Calamari.Common.Plumbing.Extensions; using Calamari.Common.Plumbing.FileSystem; using Calamari.Common.Plumbing.Logging; -using Calamari.FullFrameworkTools.Command; -using Calamari.FullFrameworkTools.Utils; -using Calamari.FullFrameworkTools.WindowsCertStore; -using Calamari.FullFrameworkTools.WindowsCertStore.WindowsNative; +using Calamari.Integration.Certificates; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Calamari.Integration.Iis +namespace Calamari.Integration.FullFramework { - public interface ILegacyFrameworkInvoker - { - TResponse Invoke(TRequest cmd); - } public class LegacyFrameworkInvoker: ILegacyFrameworkInvoker { readonly ICalamariFileSystem fileSystem; @@ -35,7 +27,7 @@ public LegacyFrameworkInvoker(ICalamariFileSystem fileSystem) public TResponse Invoke(TRequest cmd) { - var path = @"C:\Development\OctopusDeploy\calamari-second\source\Calamari.FullFrameworkTools\bin\Debug\net462\out\Calamari.FullFrameworkTools.exe"; + var path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Calamari.FullFrameworkTools", "Calamari.FullFrameworkTools.exe"); var json = JsonConvert.SerializeObject(cmd); using (var tempDir = TemporaryDirectory.Create()) @@ -107,27 +99,5 @@ enum LogLevel Fatal, // Used for Exceptions Result, // Special Response } - - - - - } - - public class InProcessInvoker: ILegacyFrameworkInvoker - { - public TResponse Invoke(TRequest cmd) - { - var log = new InnerLog(); - var commandLocator = new RequestTypeLocator(); - var commandHandler = new CommandHandler(null, new Calamari.FullFrameworkTools.Iis.InternetInformationServer()); - var requestInvoker = new CommandRequestInvoker(commandLocator, commandHandler); - - var json = JsonConvert.SerializeObject(cmd); - - var result = requestInvoker.Run(nameof(OverwriteHomeDirectoryRequest), json); - return (TResponse)result; - } } - -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/source/Calamari/Integration/FullFramework/OverwriteHomeDirectoryRequest.cs b/source/Calamari/Integration/FullFramework/OverwriteHomeDirectoryRequest.cs new file mode 100644 index 000000000..24e559b5a --- /dev/null +++ b/source/Calamari/Integration/FullFramework/OverwriteHomeDirectoryRequest.cs @@ -0,0 +1,19 @@ +using System; + +namespace Calamari.Integration.FullFramework +{ + + public class OverwriteHomeDirectoryRequest : IRequest + { + public OverwriteHomeDirectoryRequest(string iisWebSiteName, string path, bool legacySupport) + { + IisWebSiteName = iisWebSiteName; + Path = path; + LegacySupport = legacySupport; + } + + public string IisWebSiteName { get; set; } + public string Path { get; set; } + public bool LegacySupport { get; set; } + } +} \ No newline at end of file diff --git a/source/Calamari/Integration/Iis/LegacyInternetInformationServer.cs b/source/Calamari/Integration/Iis/LegacyInternetInformationServer.cs index db1c7e0e0..6070950a0 100644 --- a/source/Calamari/Integration/Iis/LegacyInternetInformationServer.cs +++ b/source/Calamari/Integration/Iis/LegacyInternetInformationServer.cs @@ -3,46 +3,22 @@ using System.Collections.Generic; using System.Linq; using System.Security.Cryptography.X509Certificates; -using Calamari.Common.Plumbing.Logging; -using Calamari.FullFrameworkTools.WindowsCertStore; -using Calamari.FullFrameworkTools.WindowsCertStore.WindowsNative; -using ILog = Calamari.FullFrameworkTools.Command.ILog; +using Calamari.Integration.Certificates; +using Calamari.Integration.FullFramework; namespace Calamari.Integration.Iis { - class InnerLog : ILog - { - public void Verbose(string message) - { - Log.Verbose(message); - } - - public void Error(string message) - { - Log.Error(message); - } - - public void Info(string message) - { - Log.Info(message); - } - - public void Fatal(Exception exception) - { - throw new NotImplementedException("This should not be handled in-process"); - } - - public void Result(object response) - { - throw new NotImplementedException("This should not be handled in-process"); - } - } - + + public class StringResponse { public string Value { get; set; } } + public class VoidResponse { } + + public class BoolResponse { public bool Value { get; set; } } + public class LegacyInternetInformationServer : IInternetInformationServer { - readonly InProcessInvoker processInvoker; + readonly ILegacyFrameworkInvoker processInvoker; - public LegacyInternetInformationServer(InProcessInvoker processInvoker) + public LegacyInternetInformationServer(ILegacyFrameworkInvoker processInvoker) { this.processInvoker = processInvoker; } @@ -55,12 +31,11 @@ public bool OverwriteHomeDirectory(string iisWebSiteName, string path, bool lega } } - public class LegacyWindowsX509CertificateStore : IWindowsX509CertificateStore { - readonly InProcessInvoker processInvoker; + readonly ILegacyFrameworkInvoker processInvoker; - public LegacyWindowsX509CertificateStore(InProcessInvoker processInvoker) + public LegacyWindowsX509CertificateStore(ILegacyFrameworkInvoker processInvoker) { this.processInvoker = processInvoker; } @@ -69,7 +44,7 @@ public string FindCertificateStore(string thumbprint, StoreLocation storeLocatio { var cmd = new FindCertificateStoreRequest(thumbprint, storeLocation); var response = processInvoker.Invoke(cmd); - return response.Valus; + return response.Value; } public void ImportCertificateToStore(byte[] pfxBytes, @@ -78,13 +53,15 @@ public void ImportCertificateToStore(byte[] pfxBytes, string storeName, bool privateKeyExportable) { - + var cmd = new ImportCertificateToStoreByLocationRequest(pfxBytes, password, storeLocation, storeName, privateKeyExportable); + processInvoker.Invoke(cmd); } public void AddPrivateKeyAccessRules(string thumbprint, StoreLocation storeLocation, ICollection privateKeyAccessRules) { - throw new NotImplementedException(); + var cmd = new AddPrivateKeyAccessRulesRequest(thumbprint, storeLocation, null, privateKeyAccessRules.ToList()); + processInvoker.Invoke(cmd); } public void AddPrivateKeyAccessRules(string thumbprint, StoreLocation storeLocation, string storeName, ICollection privateKeyAccessRules) diff --git a/source/Calamari/Program.cs b/source/Calamari/Program.cs index 9202f3777..837b19177 100644 --- a/source/Calamari/Program.cs +++ b/source/Calamari/Program.cs @@ -26,6 +26,8 @@ using IContainer = Autofac.IContainer; using Calamari.Aws.Deployment; using Calamari.Azure.Kubernetes.Discovery; +using Calamari.Integration.FullFramework; +using Calamari.Integration.Iis; using Calamari.Kubernetes.Commands.Executors; namespace Calamari @@ -71,10 +73,15 @@ protected override void ConfigureContainer(ContainerBuilder builder, CommonOptio builder.RegisterType().As().SingleInstance(); builder.RegisterType().AsSelf(); builder.RegisterType().As().SingleInstance(); - + + //TODO: Once this runs on both netcore and full framework, this can be converted to a runtime conditional check #if WINDOWS_CERTIFICATE_STORE_SUPPORT - builder.RegisterType().As().SingleInstance(); + + builder.RegisterType().As().SingleInstance(); + + //builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); #else builder.RegisterType().As().SingleInstance(); #endif From 3470942446527ba6b23ba0284eb01c677c4b4ae6 Mon Sep 17 00:00:00 2001 From: Robert E Date: Sun, 8 Sep 2024 13:13:28 +1000 Subject: [PATCH 5/8] Real error --- source/Calamari.FullFrameworkTools/Program.cs | 3 ++- .../Fixtures/Deployment/DeployPackageFixture.cs | 2 +- .../FullFramework/LegacyFrameworkInvoker.cs | 10 +++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/source/Calamari.FullFrameworkTools/Program.cs b/source/Calamari.FullFrameworkTools/Program.cs index 3797b340e..0528dcbc6 100644 --- a/source/Calamari.FullFrameworkTools/Program.cs +++ b/source/Calamari.FullFrameworkTools/Program.cs @@ -20,7 +20,7 @@ public static int Main(string[] args) { var fileVersionInfo = FileVersionInfo.GetVersionInfo(typeof(Program).Assembly.Location); log.Info(fileVersionInfo.ProductVersion); - return 1; + return 0; } var commandLocator = new RequestTypeLocator(); @@ -34,6 +34,7 @@ public static int Main(string[] args) catch (Exception ex) { log.Fatal(ex); + return -1; } return 0; } diff --git a/source/Calamari.Tests/Fixtures/Deployment/DeployPackageFixture.cs b/source/Calamari.Tests/Fixtures/Deployment/DeployPackageFixture.cs index 577d82c34..41891a955 100644 --- a/source/Calamari.Tests/Fixtures/Deployment/DeployPackageFixture.cs +++ b/source/Calamari.Tests/Fixtures/Deployment/DeployPackageFixture.cs @@ -48,7 +48,7 @@ protected CalamariResult DeployPackage(string packageName) { Variables.Save(variablesFile.FilePath); - return Invoke(Calamari() + return InvokeInProcess(Calamari() .Action("deploy-package") .Argument("package", packageName) .Argument("variables", variablesFile.FilePath)); diff --git a/source/Calamari/Integration/FullFramework/LegacyFrameworkInvoker.cs b/source/Calamari/Integration/FullFramework/LegacyFrameworkInvoker.cs index 4b04ebe8c..1e4aebf1f 100644 --- a/source/Calamari/Integration/FullFramework/LegacyFrameworkInvoker.cs +++ b/source/Calamari/Integration/FullFramework/LegacyFrameworkInvoker.cs @@ -41,7 +41,8 @@ public TResponse Invoke(TRequest cmd) fileSystem.WriteAllBytes(file.FilePath, encRequestObj); var args = new[] { cmd.GetType().Name, "--password", password, "--file", file.FilePath }; - var taskResult = ""; + var taskResult = string.Empty; + var error = string.Empty; var processResult = SilentProcessRunner.ExecuteCommand(path, string.Join(" ", args), tempDir.DirectoryPath, @@ -70,12 +71,14 @@ public TResponse Invoke(TRequest cmd) Log.Error(line["Message"]?.ToString() ?? string.Empty); Log.Error(line["Type"]?.ToString() ?? string.Empty); Log.Error(line["StackTrace"]?.ToString() ?? string.Empty); - throw new Exception(line["Message"]?.ToString()); + error = line["Message"]?.ToString(); + throw new Exception(error); case LogLevel.Result: taskResult = line["Result"]?.ToString(); break; default: - throw new ArgumentOutOfRangeException(); + error = $"Unknown log level {logLevel}"; + break; } }, Log.Error); @@ -84,6 +87,7 @@ public TResponse Invoke(TRequest cmd) { throw new Exception("Operation failed: "+ processResult.ErrorOutput); } + return JsonConvert.DeserializeObject(taskResult); } } From eb1da3da48615d818678e3fd9c464d84604e033a Mon Sep 17 00:00:00 2001 From: Robert E Date: Sun, 8 Sep 2024 13:32:05 +1000 Subject: [PATCH 6/8] Serializing identites --- .../WindowsCertStore/PrivateKeyAccessRule.cs | 32 +++++++++++------- .../WindowsX509CertificateStore.cs | 2 +- .../Certificates/PrivateKeyAccessRule.cs | 33 ++++++++++++++++--- .../WindowsX509CertificateStore.cs | 6 ++-- .../Deployment/DeployPackageFixture.cs | 2 +- .../IisWebSiteAfterPostDeployFeature.cs | 12 +++---- 6 files changed, 60 insertions(+), 27 deletions(-) diff --git a/source/Calamari.FullFrameworkTools/WindowsCertStore/PrivateKeyAccessRule.cs b/source/Calamari.FullFrameworkTools/WindowsCertStore/PrivateKeyAccessRule.cs index 8ba42d8bf..294af3269 100644 --- a/source/Calamari.FullFrameworkTools/WindowsCertStore/PrivateKeyAccessRule.cs +++ b/source/Calamari.FullFrameworkTools/WindowsCertStore/PrivateKeyAccessRule.cs @@ -11,26 +11,34 @@ public class PrivateKeyAccessRule { [JsonConstructor] public PrivateKeyAccessRule(string identity, PrivateKeyAccess access) - :this(new NTAccount(identity), access) - { - } - - public PrivateKeyAccessRule(IdentityReference identity, PrivateKeyAccess access) { Identity = identity; Access = access; } - - public IdentityReference Identity { get; } + public PrivateKeyAccess Access { get; } - - private static JsonSerializerSettings JsonSerializerSettings => new JsonSerializerSettings + public string Identity { get; } + + + public IdentityReference GetIdentityReference() { - Converters = new List + return TryParse(Identity, out var temp) ? temp! : new NTAccount(Identity); + } + + + public static bool TryParse(string value, out SecurityIdentifier? result) + { + try { - new StringEnumConverter(), + result = new SecurityIdentifier(value); + return true; } - }; + catch (ArgumentException) + { + result = null; + return false; + } + } } diff --git a/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsX509CertificateStore.cs b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsX509CertificateStore.cs index 6ce6c384b..d9b03426b 100644 --- a/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsX509CertificateStore.cs +++ b/source/Calamari.FullFrameworkTools/WindowsCertStore/WindowsX509CertificateStore.cs @@ -108,7 +108,7 @@ public void ImportCertificateToStore(byte[] pfxBytes, string password, string us if (certificate.HasPrivateKey()) { // Because we have to store the private-key in the machine key-store, we must grant the user access to it - var keySecurity = new[] {new PrivateKeyAccessRule(account, PrivateKeyAccess.FullControl)}; + var keySecurity = new[] {new PrivateKeyAccessRule(account.Value, PrivateKeyAccess.FullControl)}; AddPrivateKeyAccessRules(keySecurity, certificate); } } diff --git a/source/Calamari.Shared/Integration/Certificates/PrivateKeyAccessRule.cs b/source/Calamari.Shared/Integration/Certificates/PrivateKeyAccessRule.cs index 677ad6336..d5ddd6b43 100644 --- a/source/Calamari.Shared/Integration/Certificates/PrivateKeyAccessRule.cs +++ b/source/Calamari.Shared/Integration/Certificates/PrivateKeyAccessRule.cs @@ -10,17 +10,42 @@ public class PrivateKeyAccessRule { [JsonConstructor] public PrivateKeyAccessRule(string identity, PrivateKeyAccess access) - :this(new NTAccount(identity), access) + { + Identity = identity; + Access = access; } - public PrivateKeyAccessRule(IdentityReference identity, PrivateKeyAccess access) + /*public PrivateKeyAccessRule(IdentityReference identity, PrivateKeyAccess access) { Identity = identity; Access = access; - } + }*/ - public IdentityReference Identity { get; } + /*public IdentityReference Identity { get; }*/ + public string Identity { get; } + + public IdentityReference GetIdentityReference() + { + return TryParse(Identity, out var temp) ? (IdentityReference)temp : new NTAccount(Identity); + } + + + public static bool TryParse(string value, out SecurityIdentifier result) + { + try + { + result = new SecurityIdentifier(value); + return true; + } + catch (ArgumentException) + { + result = null; + return false; + } + } + + public PrivateKeyAccess Access { get; } public static ICollection FromJson(string json) diff --git a/source/Calamari.Shared/Integration/Certificates/WindowsX509CertificateStore.cs b/source/Calamari.Shared/Integration/Certificates/WindowsX509CertificateStore.cs index fde07eb9a..a71e9e466 100644 --- a/source/Calamari.Shared/Integration/Certificates/WindowsX509CertificateStore.cs +++ b/source/Calamari.Shared/Integration/Certificates/WindowsX509CertificateStore.cs @@ -97,7 +97,7 @@ public void ImportCertificateToStore(byte[] pfxBytes, string password, string us if (certificate.HasPrivateKey()) { // Because we have to store the private-key in the machine key-store, we must grant the user access to it - var keySecurity = new[] {new PrivateKeyAccessRule(account, PrivateKeyAccess.FullControl)}; + var keySecurity = new[] {new PrivateKeyAccessRule(userName, PrivateKeyAccess.FullControl)}; AddPrivateKeyAccessRules(keySecurity, certificate); } } @@ -439,11 +439,11 @@ static CryptoKeyAccessRule ToCryptoKeyAccessRule(PrivateKeyAccessRule rule) switch (rule.Access) { case PrivateKeyAccess.ReadOnly: - return new CryptoKeyAccessRule(rule.Identity, CryptoKeyRights.GenericRead, AccessControlType.Allow); + return new CryptoKeyAccessRule(rule.GetIdentityReference(), CryptoKeyRights.GenericRead, AccessControlType.Allow); case PrivateKeyAccess.FullControl: // We use 'GenericAll' here rather than 'FullControl' as 'FullControl' doesn't correctly set the access for CNG keys - return new CryptoKeyAccessRule(rule.Identity, CryptoKeyRights.GenericAll, AccessControlType.Allow); + return new CryptoKeyAccessRule(rule.GetIdentityReference(), CryptoKeyRights.GenericAll, AccessControlType.Allow); default: throw new ArgumentOutOfRangeException(nameof(rule.Access)); diff --git a/source/Calamari.Tests/Fixtures/Deployment/DeployPackageFixture.cs b/source/Calamari.Tests/Fixtures/Deployment/DeployPackageFixture.cs index 41891a955..577d82c34 100644 --- a/source/Calamari.Tests/Fixtures/Deployment/DeployPackageFixture.cs +++ b/source/Calamari.Tests/Fixtures/Deployment/DeployPackageFixture.cs @@ -48,7 +48,7 @@ protected CalamariResult DeployPackage(string packageName) { Variables.Save(variablesFile.FilePath); - return InvokeInProcess(Calamari() + return Invoke(Calamari() .Action("deploy-package") .Argument("package", packageName) .Argument("variables", variablesFile.FilePath)); diff --git a/source/Calamari/Deployment/Features/IisWebSiteAfterPostDeployFeature.cs b/source/Calamari/Deployment/Features/IisWebSiteAfterPostDeployFeature.cs index 11a271a93..1761893f5 100644 --- a/source/Calamari/Deployment/Features/IisWebSiteAfterPostDeployFeature.cs +++ b/source/Calamari/Deployment/Features/IisWebSiteAfterPostDeployFeature.cs @@ -66,7 +66,7 @@ static PrivateKeyAccessRule CreatePrivateKeyAccessForApplicationPoolAccount(IVar PrivateKeyAccess.FullControl); } - static IdentityReference GetIdentityForApplicationPoolIdentity(ApplicationPoolIdentityType applicationPoolIdentityType, + static string GetIdentityForApplicationPoolIdentity(ApplicationPoolIdentityType applicationPoolIdentityType, IVariables variables) { //TODO: Once this only runs netcore we can remove this check (or potentially externalize the whole check) @@ -79,19 +79,19 @@ static IdentityReference GetIdentityForApplicationPoolIdentity(ApplicationPoolId switch (applicationPoolIdentityType) { case ApplicationPoolIdentityType.ApplicationPoolIdentity: - return new NTAccount("IIS AppPool\\" + variables.Get(SpecialVariables.Action.IisWebSite.ApplicationPoolName)); + return new NTAccount("IIS AppPool\\" + variables.Get(SpecialVariables.Action.IisWebSite.ApplicationPoolName)).Value; case ApplicationPoolIdentityType.LocalService: - return new SecurityIdentifier(WellKnownSidType.LocalServiceSid, null); + return new SecurityIdentifier(WellKnownSidType.LocalServiceSid, null).Value; case ApplicationPoolIdentityType.LocalSystem: - return new SecurityIdentifier(WellKnownSidType.LocalServiceSid, null); + return new SecurityIdentifier(WellKnownSidType.LocalServiceSid, null).Value; case ApplicationPoolIdentityType.NetworkService: - return new SecurityIdentifier(WellKnownSidType.NetworkServiceSid, null); + return new SecurityIdentifier(WellKnownSidType.NetworkServiceSid, null).Value; case ApplicationPoolIdentityType.SpecificUser: - return new NTAccount(StripLocalAccountIdentifierFromUsername(variables.Get(SpecialVariables.Action.IisWebSite.ApplicationPoolUserName))); + return new NTAccount(StripLocalAccountIdentifierFromUsername(variables.Get(SpecialVariables.Action.IisWebSite.ApplicationPoolUserName))).Value; default: throw new ArgumentOutOfRangeException(nameof(applicationPoolIdentityType), applicationPoolIdentityType, null); From 19d981cf1d29c62c5cc8f397888bc28dbb11ecb4 Mon Sep 17 00:00:00 2001 From: Robert E Date: Sun, 8 Sep 2024 22:52:55 +1000 Subject: [PATCH 7/8] Update cert test --- .../Command/CommandHandler.cs | 10 ++---- source/Calamari.FullFrameworkTools/Program.cs | 34 +++++++++++-------- .../Contracts/FindCertificateStoreRequest.cs | 8 +++-- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/source/Calamari.FullFrameworkTools/Command/CommandHandler.cs b/source/Calamari.FullFrameworkTools/Command/CommandHandler.cs index d9af2edab..daee75110 100644 --- a/source/Calamari.FullFrameworkTools/Command/CommandHandler.cs +++ b/source/Calamari.FullFrameworkTools/Command/CommandHandler.cs @@ -42,10 +42,6 @@ public object Handle(IRequest obj) } } -public interface IFullFrameworkToolResponse - { - } - - public class StringResponse : IFullFrameworkToolResponse { public string? Value { get; set; } } - public class VoidResponse : IFullFrameworkToolResponse { } - public class BoolResponse : IFullFrameworkToolResponse { public bool Value { get; set; } } + public class StringResponse { public string? Value { get; set; } } + public class VoidResponse { } + public class BoolResponse { public bool Value { get; set; } } diff --git a/source/Calamari.FullFrameworkTools/Program.cs b/source/Calamari.FullFrameworkTools/Program.cs index 0528dcbc6..07ecd712c 100644 --- a/source/Calamari.FullFrameworkTools/Program.cs +++ b/source/Calamari.FullFrameworkTools/Program.cs @@ -8,21 +8,8 @@ namespace Calamari.FullFrameworkTools { public class Program { - public static int Main(string[] args) + public int Run(string cmd, string password, string file) { - var log = new SerializedLog(); - if (!ExtractArgs(args, out var cmd, out var password, out var file)) - { - WriteHelp(); - return -1; - } - if (cmd == "version") - { - var fileVersionInfo = FileVersionInfo.GetVersionInfo(typeof(Program).Assembly.Location); - log.Info(fileVersionInfo.ProductVersion); - return 0; - } - var commandLocator = new RequestTypeLocator(); var commandHandler = new CommandHandler(new WindowsX509CertificateStore(log), new InternetInformationServer()); var requestInvoker = new CommandRequestInvoker(commandLocator, commandHandler); @@ -39,6 +26,25 @@ public static int Main(string[] args) return 0; } + static readonly ILog log = new SerializedLog(); + + public static int Main(string[] args) + { + if (!ExtractArgs(args, out var cmd, out var password, out var file)) + { + WriteHelp(); + return -1; + } + if (cmd == "version") + { + var fileVersionInfo = FileVersionInfo.GetVersionInfo(typeof(Program).Assembly.Location); + log.Info(fileVersionInfo.ProductVersion); + return 0; + } + + return new Program().Run(cmd, password, file); + } + static void WriteHelp() { Console.Error.WriteLine("Commands: iis, win-cert-store, version"); diff --git a/source/Calamari.FullFrameworkTools/WindowsCertStore/Contracts/FindCertificateStoreRequest.cs b/source/Calamari.FullFrameworkTools/WindowsCertStore/Contracts/FindCertificateStoreRequest.cs index 594725bda..a45a8f733 100644 --- a/source/Calamari.FullFrameworkTools/WindowsCertStore/Contracts/FindCertificateStoreRequest.cs +++ b/source/Calamari.FullFrameworkTools/WindowsCertStore/Contracts/FindCertificateStoreRequest.cs @@ -16,9 +16,11 @@ public FindCertificateStoreRequest(string thumbprint, StoreLocation storeLocatio public string Thumbprint { get; } public StoreLocation StoreLocation { get; } - public VoidResponse DoIt(IWindowsX509CertificateStore certificateStore) + public StringResponse DoIt(IWindowsX509CertificateStore certificateStore) { - certificateStore.FindCertificateStore(Thumbprint, StoreLocation); - return new VoidResponse(); + var result =certificateStore.FindCertificateStore(Thumbprint, StoreLocation); + return new StringResponse() { + Value = result + }; } } \ No newline at end of file From da4cd9dc221ccb7e0a6fb714ed352052ff40e49d Mon Sep 17 00:00:00 2001 From: Robert E Date: Mon, 9 Sep 2024 21:47:37 +1000 Subject: [PATCH 8/8] revert --- source/Calamari.Tests/Logger/AbstractLogFixture.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/Calamari.Tests/Logger/AbstractLogFixture.cs b/source/Calamari.Tests/Logger/AbstractLogFixture.cs index 75a313bd5..7c9cf2583 100644 --- a/source/Calamari.Tests/Logger/AbstractLogFixture.cs +++ b/source/Calamari.Tests/Logger/AbstractLogFixture.cs @@ -18,7 +18,7 @@ public void AddValueToRedact_ReplacesRedactedStringWithGivenPlaceholder() { const string placeholder = ""; Func logMessage = s => $"here is my super cool message with {s} but is it redacted?"; - var log = new TestSerializedLog(); + var log = new TestLog(); log.AddValueToRedact(Password, placeholder); log.Info(logMessage(Password)); @@ -31,7 +31,7 @@ public void AddValueToRedact_RedactsAllInstancesOfGivenString_WhenAddedBeforeLog { const string logMessage = "here is a message with " + Password + " and other text"; const string logFormatString = "here is a log format string {0} where stuff is added in the middle"; - var log = new TestSerializedLog(); + var log = new TestLog(); log.AddValueToRedact(Password, ""); log.Error(logMessage); log.ErrorFormat(logFormatString, Password); @@ -56,7 +56,7 @@ public void AddValueToRedact_ValueReplacementsCanBeUpdated() act.Should().NotThrow(because: "you can update the placeholder for a given redacted value."); } - public class TestSerializedLog : AbstractLog + public class TestLog : AbstractLog { public List AllOutput { get; } = new List(); protected override void StdOut(string message)