diff options
author | Wouter Tinus <wouter.tinus@gmail.com> | 2020-04-19 20:31:39 +0200 |
---|---|---|
committer | Wouter Tinus <wouter.tinus@gmail.com> | 2020-04-19 20:31:39 +0200 |
commit | 9129b89b2af96111ae05ae20db7dcf55039e97a3 (patch) | |
tree | 94c3edf5620ab89f97b64496a337b8da19705f78 | |
parent | dfb295ea176034826c6a034aba19dbabca3d33cb (diff) | |
parent | 06ef5360b246132c14554229020427ff3c5d0ac8 (diff) | |
download | letsencrypt-win-simple-9129b89b2af96111ae05ae20db7dcf55039e97a3.zip letsencrypt-win-simple-9129b89b2af96111ae05ae20db7dcf55039e97a3.tar.gz letsencrypt-win-simple-9129b89b2af96111ae05ae20db7dcf55039e97a3.tar.bz2 |
Merge remote-tracking branch 'origin/2.1.7' into pr/1486-add-support-for-current-azure-environments
70 files changed, 1570 insertions, 960 deletions
diff --git a/src/main.lib/Clients/Acme/AcmeClient.cs b/src/main.lib/Clients/Acme/AcmeClient.cs index 5e0385a..2eae016 100644 --- a/src/main.lib/Clients/Acme/AcmeClient.cs +++ b/src/main.lib/Clients/Acme/AcmeClient.cs @@ -38,6 +38,8 @@ namespace PKISharp.WACS.Clients.Acme public const string AuthorizationPending = "pending"; public const string AuthorizationProcessing = "processing"; + public const string ChallengeValid = "valid"; + private readonly ILogService _log; private readonly IInputService _input; private readonly ISettingsService _settings; diff --git a/src/main.lib/Clients/Acme/OrderManager.cs b/src/main.lib/Clients/Acme/OrderManager.cs index a0f9c2b..b57ac8f 100644 --- a/src/main.lib/Clients/Acme/OrderManager.cs +++ b/src/main.lib/Clients/Acme/OrderManager.cs @@ -39,9 +39,9 @@ namespace PKISharp.WACS.Clients.Acme /// <param name="renewal"></param> /// <param name="target"></param> /// <returns></returns> - public async Task<OrderDetails?> GetOrCreate(Renewal renewal, Target target, RunLevel runLevel) + public async Task<OrderDetails?> GetOrCreate(Order order, RunLevel runLevel) { - var cacheKey = _certificateService.CacheKey(renewal, target); + var cacheKey = _certificateService.CacheKey(order); var existingOrder = FindRecentOrder(cacheKey); if (existingOrder != null) { @@ -65,7 +65,7 @@ namespace PKISharp.WACS.Clients.Acme } else { - _log.Debug($"Cached order has status {existingOrder.Payload.Status}, discarding"); + _log.Debug("Cached order has status {status}, discarding", existingOrder.Payload.Status); } } } @@ -74,7 +74,7 @@ namespace PKISharp.WACS.Clients.Acme _log.Warning("Unable to refresh cached order: {ex}", ex.Message); } } - var identifiers = target.GetHosts(false); + var identifiers = order.Target.GetHosts(false); return await CreateOrder(identifiers, cacheKey); } diff --git a/src/main.lib/Clients/IIS/IISHttpBindingUpdater.cs b/src/main.lib/Clients/IIS/IISHttpBindingUpdater.cs index 43945c3..b2b5ca3 100644 --- a/src/main.lib/Clients/IIS/IISHttpBindingUpdater.cs +++ b/src/main.lib/Clients/IIS/IISHttpBindingUpdater.cs @@ -61,10 +61,23 @@ namespace PKISharp.WACS.Clients.IIS { try { - found.Add(binding.Host); - if (UpdateBinding(site, binding, bindingOptions)) + // Only update if the old binding actually matches + // with the new certificate + if (identifiers.Any(i => Fits(binding.Host, i, SSLFlags.None) > 0)) { - bindingsUpdated += 1; + found.Add(binding.Host); + if (UpdateBinding(site, binding, bindingOptions)) + { + bindingsUpdated += 1; + } + } + else + { + _log.Warning( + "Existing https binding {host}:{port}{ip} not updated because it doesn't seem to match the new certificate!", + binding.Host, + binding.Port, + string.IsNullOrEmpty(binding.IP) ? "" : $":{binding.IP}"); } } catch (Exception ex) @@ -93,7 +106,7 @@ namespace PKISharp.WACS.Clients.IIS var current = todo.First(); try { - var (hostFound, commitRequired) = AddOrUpdateBindings( + var (hostFound, bindings) = AddOrUpdateBindings( allBindings.Select(x => x.binding).ToArray(), targetSite, bindingOptions.WithHost(current)); @@ -111,10 +124,7 @@ namespace PKISharp.WACS.Clients.IIS else { found.Add(hostFound); - if (commitRequired) - { - bindingsUpdated += 1; - } + bindingsUpdated += bindings; } } catch (Exception ex) @@ -148,7 +158,7 @@ namespace PKISharp.WACS.Clients.IIS /// <param name="port"></param> /// <param name="ipAddress"></param> /// <param name="fuzzy"></param> - private (string?, bool) AddOrUpdateBindings(TBinding[] allBindings, TSite site, BindingOptions bindingOptions) + private (string?, int) AddOrUpdateBindings(TBinding[] allBindings, TSite site, BindingOptions bindingOptions) { if (bindingOptions.Host == null) { @@ -156,7 +166,7 @@ namespace PKISharp.WACS.Clients.IIS } // Require IIS manager to commit - var commitRequired = false; + var commit = 0; // Get all bindings which could map to the host var matchingBindings = site.Bindings. @@ -186,7 +196,10 @@ namespace PKISharp.WACS.Clients.IIS if (UpdateExistingBindingFlags(bindingOptions.Flags, match.binding, allBindings, out var updateFlags)) { var updateOptions = bindingOptions.WithFlags(updateFlags); - commitRequired = UpdateBinding(site, match.binding, updateOptions); + if (UpdateBinding(site, match.binding, updateOptions)) + { + commit++; + } } } else @@ -206,11 +219,11 @@ namespace PKISharp.WACS.Clients.IIS { AddBinding(site, addOptions); existing.Add(binding); - commitRequired = true; + commit++; } } } - return (bestMatch.binding.Host, commitRequired); + return (bestMatch.binding.Host, commit); } } @@ -219,12 +232,12 @@ namespace PKISharp.WACS.Clients.IIS if (AllowAdd(bindingOptions, allBindings)) { AddBinding(site, bindingOptions); - commitRequired = true; - return (bindingOptions.Host, commitRequired); + commit++; + return (bindingOptions.Host, commit); } // We haven't been able to do anything - return (null, commitRequired); + return (null, commit); } /// <summary> @@ -407,9 +420,10 @@ namespace PKISharp.WACS.Clients.IIS preserveFlags &= ~SSLFlags.NotWithCentralSsl; } options = options.WithFlags(options.Flags | preserveFlags); - _log.Information(LogType.All, "Updating existing https binding {host}:{port} (flags: {flags})", + _log.Information(LogType.All, "Updating existing https binding {host}:{port}{ip} (flags: {flags})", existingBinding.Host, existingBinding.Port, + string.IsNullOrEmpty(existingBinding.IP) ? "" : $":{existingBinding.IP}", (int)options.Flags); _client.UpdateBinding(site, existingBinding, options); return true; diff --git a/src/main.lib/Configuration/MainArguments.cs b/src/main.lib/Configuration/MainArguments.cs index ba6ae22..e446d8a 100644 --- a/src/main.lib/Configuration/MainArguments.cs +++ b/src/main.lib/Configuration/MainArguments.cs @@ -26,6 +26,7 @@ namespace PKISharp.WACS.Configuration public string? Target { get; set; } public string? Validation { get; set; } public string? ValidationMode { get; set; } + public string? Order { get; set; } public string? Csr { get; set; } public string? Store { get; set; } public string? Installation { get; set; } diff --git a/src/main.lib/Configuration/MainArgumentsProvider.cs b/src/main.lib/Configuration/MainArgumentsProvider.cs index 2f02081..6fd17fa 100644 --- a/src/main.lib/Configuration/MainArgumentsProvider.cs +++ b/src/main.lib/Configuration/MainArgumentsProvider.cs @@ -15,6 +15,7 @@ namespace PKISharp.WACS.Configuration !string.IsNullOrEmpty(current.FriendlyName) || !string.IsNullOrEmpty(current.Installation) || !string.IsNullOrEmpty(current.Store) || + !string.IsNullOrEmpty(current.Order) || !string.IsNullOrEmpty(current.Csr) || !string.IsNullOrEmpty(current.Target) || !string.IsNullOrEmpty(current.Validation); @@ -98,10 +99,14 @@ namespace PKISharp.WACS.Configuration .As("validationmode") .SetDefault(Constants.Http01ChallengeType) .WithDescription("Specify which validation mode to use. HTTP-01 is the default."); + + parser.Setup(o => o.Order) + .As("order") + .WithDescription("Specify which order plugin to use. Single is the default."); parser.Setup(o => o.Csr) .As("csr") - .WithDescription("Specify which csr plugin to use. RSA is the default."); + .WithDescription("Specify which CSR plugin to use. RSA is the default."); parser.Setup(o => o.Store) .As("store") diff --git a/src/main.lib/DomainObjects/Order.cs b/src/main.lib/DomainObjects/Order.cs new file mode 100644 index 0000000..cb7cdc5 --- /dev/null +++ b/src/main.lib/DomainObjects/Order.cs @@ -0,0 +1,25 @@ +using acme = ACMESharp.Protocol; + +namespace PKISharp.WACS.DomainObjects +{ + public class Order + { + public string? CacheKeyPart { get; set; } + public string? FriendlyNamePart { get; set; } + public Target Target { get; set; } + public Renewal Renewal { get; set; } + public acme.OrderDetails? Details { get; set; } = null; + + public Order( + Renewal renewal, + Target target, + string? cacheKeyPart = null, + string? friendlyNamePart = null) + { + Target = target; + Renewal = renewal; + CacheKeyPart = cacheKeyPart; + FriendlyNamePart = friendlyNamePart; + } + } +} diff --git a/src/main.lib/DomainObjects/RenewResult.cs b/src/main.lib/DomainObjects/RenewResult.cs index 8ab75eb..4f1bc72 100644 --- a/src/main.lib/DomainObjects/RenewResult.cs +++ b/src/main.lib/DomainObjects/RenewResult.cs @@ -1,32 +1,69 @@ using PKISharp.WACS.Extensions; using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; namespace PKISharp.WACS.DomainObjects { public class RenewResult { public DateTime Date { get; set; } + + [JsonIgnore] + public bool Abort { get; set; } + public bool Success { get; set; } - public string? ErrorMessage { get; set; } - public string? Thumbprint { get; set; } - private RenewResult() => Date = DateTime.UtcNow; + public string? ErrorMessage { set => AddErrorMessage(value); } + + public string? Thumbprint { set => AddThumbprint(value); } + + public RenewResult AddErrorMessage(string? value, bool fatal = true) + { + if (value != null) + { + if (!ErrorMessages.Contains(value)) + { + ErrorMessages.Add(value); + } + } + if (fatal) + { + Success = false; + } + return this; + } + + public void AddThumbprint(string? value) + { + if (value != null) + { + if (!Thumbprints.Contains(value)) + { + Thumbprints.Add(value); + } + } + } + + public List<string> Thumbprints { get; set; } = new List<string>(); + public List<string> ErrorMessages { get; set; } = new List<string>(); - public RenewResult(CertificateInfo certificate) : this() + public RenewResult() { Success = true; - Thumbprint = certificate.Certificate.Thumbprint; + Date = DateTime.UtcNow; } public RenewResult(string error) : this() { Success = false; - ErrorMessage = error; + AddErrorMessage(error); } public override string ToString() => $"{Date} " + $"- {(Success ? "Success" : "Error")}" + - $"{(string.IsNullOrEmpty(Thumbprint) ? "" : $" - Thumbprint {Thumbprint}")}" + - $"{(string.IsNullOrEmpty(ErrorMessage) ? "" : $" - {ErrorMessage.ReplaceNewLines()}")}"; + $"{(Thumbprints.Count == 0 ? "" : $" - Thumbprint {string.Join(", ", Thumbprints)}")}" + + $"{(ErrorMessages.Count == 0 ? "" : $" - {string.Join(", ", ErrorMessages.Select(x => x.ReplaceNewLines()))}")}"; } } diff --git a/src/main.lib/DomainObjects/Renewal.cs b/src/main.lib/DomainObjects/Renewal.cs index 3cd8062..cb8b32f 100644 --- a/src/main.lib/DomainObjects/Renewal.cs +++ b/src/main.lib/DomainObjects/Renewal.cs @@ -120,6 +120,11 @@ namespace PKISharp.WACS.DomainObjects public CsrPluginOptions? CsrPluginOptions { get; set; } /// <summary> + /// Store information about OrderPlugin + /// </summary> + public OrderPluginOptions? OrderPluginOptions { get; set; } + + /// <summary> /// Store information about StorePlugin /// </summary> public List<StorePluginOptions> StorePluginOptions { get; set; } = new List<StorePluginOptions>(); @@ -162,7 +167,8 @@ namespace PKISharp.WACS.DomainObjects if (errors.Count() > 0) { - ret += $", {errors.Count()} error{(errors.Count() != 1 ? "s" : "")} like \"{errors.First().ErrorMessage}\""; + var messages = errors.SelectMany(x => x.ErrorMessages).Where(x => !string.IsNullOrEmpty(x)); + ret += $", {errors.Count()} error{(errors.Count() != 1 ? "s" : "")} like \"{messages.FirstOrDefault() ?? "[null]"}\""; } return ret; } diff --git a/src/main.lib/Extensions/RenewalExtensions.cs b/src/main.lib/Extensions/RenewalExtensions.cs deleted file mode 100644 index ed28419..0000000 --- a/src/main.lib/Extensions/RenewalExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using PKISharp.WACS.DomainObjects; -using System.Linq; - -namespace PKISharp.WACS.Extensions -{ - public static class RenewalExtensions - { - /// <summary> - /// Get the most recent thumbprint - /// </summary> - /// <returns></returns> - public static string? Thumbprint(this Renewal renewal) - { - return renewal. - History?. - OrderByDescending(x => x.Date). - Where(x => x.Success). - Select(x => x.Thumbprint). - FirstOrDefault(); - } - } -} diff --git a/src/main.lib/Extensions/StringExtensions.cs b/src/main.lib/Extensions/StringExtensions.cs index 9740b3b..b9b9a84 100644 --- a/src/main.lib/Extensions/StringExtensions.cs +++ b/src/main.lib/Extensions/StringExtensions.cs @@ -112,10 +112,44 @@ namespace PKISharp.WACS.Extensions } } + /// <summary> + /// + /// </summary> + /// <param name="pattern"></param> + /// <returns></returns> public static string PatternToRegex(this string pattern) { + pattern = pattern.Replace("\\\\", SlashEscape); + pattern = pattern.Replace("\\,", CommaEscape); + pattern = pattern.Replace("\\*", StarEscape); + pattern = pattern.Replace("\\?", QuestionEscape); var parts = pattern.ParseCsv(); - return $"^({string.Join('|', parts.Select(x => Regex.Escape(x).Replace(@"\*", ".*").Replace(@"\?", ".")))})$"; + return $"^({string.Join('|', parts.Select(x => Regex.Escape(x).PatternToRegexPart()))})$"; + } + + private const string SlashEscape = "~slash~"; + private const string CommaEscape = "~comma~"; + private const string StarEscape = "~star~"; + private const string QuestionEscape = "~question~"; + + public static string EscapePattern(this string pattern) + { + return pattern. + Replace("\\", "\\\\"). + Replace(",", "\\,"). + Replace("*", "\\*"). + Replace("?", "\\?"); + } + + private static string PatternToRegexPart(this string pattern) + { + return pattern. + Replace("\\*", ".*"). + Replace("\\?", "."). + Replace(SlashEscape, "\\\\"). + Replace(CommaEscape, ","). + Replace(StarEscape, "\\*"). + Replace(QuestionEscape, "\\?"); } diff --git a/src/main.lib/Plugins/Base/Options/OrderPluginOptions.cs b/src/main.lib/Plugins/Base/Options/OrderPluginOptions.cs new file mode 100644 index 0000000..b50dfc6 --- /dev/null +++ b/src/main.lib/Plugins/Base/Options/OrderPluginOptions.cs @@ -0,0 +1,28 @@ +using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Services; +using PKISharp.WACS.Services.Serialization; +using System; + +namespace PKISharp.WACS.Plugins.Base.Options +{ + public class OrderPluginOptions : PluginOptions + { + public override string Name => throw new NotImplementedException(); + public override string Description => throw new NotImplementedException(); + public override Type Instance => throw new NotImplementedException(); + } + + public abstract class OrderPluginOptions<T> : OrderPluginOptions where T : IOrderPlugin + { + public abstract override string Name { get; } + public abstract override string Description { get; } + + public override void Show(IInputService input) + { + input.Show("Order"); + input.Show("Plugin", $"{Name} - ({Description})", level: 1); + } + + public override Type Instance => typeof(T); + } +} diff --git a/src/main.lib/Plugins/Base/OptionsFactories/CsrPluginOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/CsrPluginOptionsFactory.cs index 1aa2e0b..224da47 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/CsrPluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/CsrPluginOptionsFactory.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace PKISharp.WACS.Plugins.Base.Factories { /// <summary> - /// StorePluginFactory base implementation + /// CsrPluginFactory base implementation /// </summary> /// <typeparam name="TPlugin"></typeparam> public abstract class CsrPluginOptionsFactory<TPlugin, TOptions> : @@ -17,7 +17,7 @@ namespace PKISharp.WACS.Plugins.Base.Factories { public abstract Task<TOptions> Aquire(IInputService inputService, RunLevel runLevel); public abstract Task<TOptions> Default(); - async Task<CsrPluginOptions?> ICsrPluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); - async Task<CsrPluginOptions?> ICsrPluginOptionsFactory.Default() => await Default(); + async Task<CsrPluginOptions?> IPluginOptionsFactory<CsrPluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); + async Task<CsrPluginOptions?> IPluginOptionsFactory<CsrPluginOptions>.Default() => await Default(); } }
\ No newline at end of file diff --git a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullCsrOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullCsrOptionsFactory.cs index 3b7caf1..7d36375 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullCsrOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullCsrOptionsFactory.cs @@ -18,7 +18,7 @@ namespace PKISharp.WACS.Plugins.Base.Factories.Null int IPluginOptionsFactory.Order => int.MaxValue; (bool, string?) IPluginOptionsFactory.Disabled => (false, null); bool IPluginOptionsFactory.Match(string name) => false; - Task<CsrPluginOptions?> ICsrPluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => Task.FromResult<CsrPluginOptions?>(null); - Task<CsrPluginOptions?> ICsrPluginOptionsFactory.Default() => Task.FromResult<CsrPluginOptions?>(null); + Task<CsrPluginOptions?> IPluginOptionsFactory<CsrPluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => Task.FromResult<CsrPluginOptions?>(null); + Task<CsrPluginOptions?> IPluginOptionsFactory<CsrPluginOptions>.Default() => Task.FromResult<CsrPluginOptions?>(null); } } diff --git a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs index 93bc9e8..7adf7e2 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs @@ -36,6 +36,6 @@ namespace PKISharp.WACS.Plugins.Base.Factories.Null internal class NullInstallation : IInstallationPlugin { (bool, string?) IPlugin.Disabled => (false, null); - Task IInstallationPlugin.Install(IEnumerable<IStorePlugin> stores, CertificateInfo newCertificateInfo, CertificateInfo? oldCertificateInfo) => Task.CompletedTask; + Task IInstallationPlugin.Install(Target target, IEnumerable<IStorePlugin> stores, CertificateInfo newCertificateInfo, CertificateInfo? oldCertificateInfo) => Task.CompletedTask; } } diff --git a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullOrderOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullOrderOptionsFactory.cs new file mode 100644 index 0000000..f5958e6 --- /dev/null +++ b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullOrderOptionsFactory.cs @@ -0,0 +1,26 @@ +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Plugins.Base.Options; +using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Services; +using System; +using System.Threading.Tasks; + +namespace PKISharp.WACS.Plugins.Base.Factories.Null +{ + /// <summary> + /// Null implementation + /// </summary> + internal class NullOrderOptionsFactory : IOrderPluginOptionsFactory, INull + { + Type IPluginOptionsFactory.InstanceType => typeof(object); + Type IPluginOptionsFactory.OptionsType => typeof(object); + string IPluginOptionsFactory.Name => "None"; + string? IPluginOptionsFactory.Description => null; + int IPluginOptionsFactory.Order => int.MaxValue; + (bool, string?) IPluginOptionsFactory.Disabled => (false, null); + bool IPluginOptionsFactory.Match(string name) => false; + Task<OrderPluginOptions?> IPluginOptionsFactory<OrderPluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => Task.FromResult<OrderPluginOptions?>(null); + Task<OrderPluginOptions?> IPluginOptionsFactory<OrderPluginOptions>.Default() => Task.FromResult<OrderPluginOptions?>(null); + bool IOrderPluginOptionsFactory.CanProcess(Target target) => false; + } +} diff --git a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs index 75d3439..e98cd78 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs @@ -15,8 +15,8 @@ namespace PKISharp.WACS.Plugins.Base.Factories.Null Type IPluginOptionsFactory.InstanceType => typeof(NullStore); Type IPluginOptionsFactory.OptionsType => typeof(NullStoreOptions); Task<StorePluginOptions?> Generate() => Task.FromResult<StorePluginOptions?>(new NullStoreOptions()); - Task<StorePluginOptions?> IStorePluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => Generate(); - Task<StorePluginOptions?> IStorePluginOptionsFactory.Default() => Generate(); + Task<StorePluginOptions?> IPluginOptionsFactory<StorePluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => Generate(); + Task<StorePluginOptions?> IPluginOptionsFactory<StorePluginOptions>.Default() => Generate(); (bool, string?) IPluginOptionsFactory.Disabled => (false, null); string IPluginOptionsFactory.Name => NullStoreOptions.PluginName; string IPluginOptionsFactory.Description => new NullStoreOptions().Description; diff --git a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullTargetOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullTargetOptionsFactory.cs index 8598f1d..4920dca 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullTargetOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullTargetOptionsFactory.cs @@ -13,11 +13,10 @@ namespace PKISharp.WACS.Plugins.Base.Factories.Null { Type IPluginOptionsFactory.InstanceType => typeof(object); Type IPluginOptionsFactory.OptionsType => typeof(object); - bool ITargetPluginOptionsFactory.Hidden => true; (bool, string?) IPluginOptionsFactory.Disabled => (false, null); bool IPluginOptionsFactory.Match(string name) => false; - Task<TargetPluginOptions?> ITargetPluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => Task.FromResult<TargetPluginOptions?>(default); - Task<TargetPluginOptions?> ITargetPluginOptionsFactory.Default() => Task.FromResult<TargetPluginOptions?>(default); + Task<TargetPluginOptions?> IPluginOptionsFactory<TargetPluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => Task.FromResult<TargetPluginOptions?>(default); + Task<TargetPluginOptions?> IPluginOptionsFactory<TargetPluginOptions>.Default() => Task.FromResult<TargetPluginOptions?>(default); string IPluginOptionsFactory.Name => "None"; string? IPluginOptionsFactory.Description => null; int IPluginOptionsFactory.Order => int.MaxValue; diff --git a/src/main.lib/Plugins/Base/OptionsFactories/OrderPluginOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/OrderPluginOptionsFactory.cs new file mode 100644 index 0000000..605defc --- /dev/null +++ b/src/main.lib/Plugins/Base/OptionsFactories/OrderPluginOptionsFactory.cs @@ -0,0 +1,25 @@ +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Plugins.Base.Options; +using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Services; +using System.Threading.Tasks; + +namespace PKISharp.WACS.Plugins.Base.Factories +{ + /// <summary> + /// OrderPluginFactory base implementation + /// </summary> + /// <typeparam name="TPlugin"></typeparam> + public abstract class OrderPluginOptionsFactory<TPlugin, TOptions> : + PluginOptionsFactory<TPlugin, TOptions>, + IOrderPluginOptionsFactory + where TPlugin : IOrderPlugin + where TOptions : OrderPluginOptions, new() + { + public abstract bool CanProcess(Target target); + public abstract Task<TOptions> Aquire(IInputService inputService, RunLevel runLevel); + public abstract Task<TOptions> Default(); + async Task<OrderPluginOptions?> IPluginOptionsFactory<OrderPluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); + async Task<OrderPluginOptions?> IPluginOptionsFactory<OrderPluginOptions>.Default() => await Default(); + } +}
\ No newline at end of file diff --git a/src/main.lib/Plugins/Base/OptionsFactories/StorePluginOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/StorePluginOptionsFactory.cs index 6fcfa88..f1664b9 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/StorePluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/StorePluginOptionsFactory.cs @@ -17,8 +17,8 @@ namespace PKISharp.WACS.Plugins.Base.Factories { public abstract Task<TOptions?> Aquire(IInputService inputService, RunLevel runLevel); public abstract Task<TOptions?> Default(); - async Task<StorePluginOptions?> IStorePluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); - async Task<StorePluginOptions?> IStorePluginOptionsFactory.Default() => await Default(); + async Task<StorePluginOptions?> IPluginOptionsFactory<StorePluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); + async Task<StorePluginOptions?> IPluginOptionsFactory<StorePluginOptions>.Default() => await Default(); } diff --git a/src/main.lib/Plugins/Base/OptionsFactories/TargetPluginOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/TargetPluginOptionsFactory.cs index 1358cde..0302a82 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/TargetPluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/TargetPluginOptionsFactory.cs @@ -17,14 +17,7 @@ namespace PKISharp.WACS.Plugins.Base.Factories { public abstract Task<TOptions?> Aquire(IInputService inputService, RunLevel runLevel); public abstract Task<TOptions?> Default(); - - /// <summary> - /// Allow implementations to hide themselves from users - /// in interactive mode - /// </summary> - public virtual bool Hidden { get; protected set; } = false; - - async Task<TargetPluginOptions?> ITargetPluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); - async Task<TargetPluginOptions?> ITargetPluginOptionsFactory.Default() => await Default(); + async Task<TargetPluginOptions?> IPluginOptionsFactory<TargetPluginOptions>.Aquire(IInputService inputService, RunLevel runLevel) => await Aquire(inputService, runLevel); + async Task<TargetPluginOptions?> IPluginOptionsFactory<TargetPluginOptions>.Default() => await Default(); } } diff --git a/src/main.lib/Plugins/Base/OptionsFactories/ValidationPluginOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/ValidationPluginOptionsFactory.cs index 690c89a..131bd7f 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/ValidationPluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/ValidationPluginOptionsFactory.cs @@ -16,7 +16,6 @@ namespace PKISharp.WACS.Plugins.Base.Factories { private readonly string _challengeType; string IValidationPluginOptionsFactory.ChallengeType => _challengeType; - public virtual bool Hidden => false; public ValidationPluginOptionsFactory(string challengeType = Constants.Http01ChallengeType) => _challengeType = challengeType; public abstract Task<TOptions?> Aquire(Target target, IInputService inputService, RunLevel runLevel); diff --git a/src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtp.cs b/src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtp.cs index 4d575fb..fc91e0a 100644 --- a/src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtp.cs +++ b/src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtp.cs @@ -25,7 +25,7 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins _userRoleService = userRoleService; } - Task IInstallationPlugin.Install(IEnumerable<IStorePlugin> stores, CertificateInfo newCertificate, CertificateInfo? oldCertificate) + Task IInstallationPlugin.Install(Target target, IEnumerable<IStorePlugin> stores, CertificateInfo newCertificate, CertificateInfo? oldCertificate) { if (!stores.Any(x => x is CertificateStore)) { diff --git a/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWeb.cs b/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWeb.cs index 035124a..bb55032 100644 --- a/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWeb.cs +++ b/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWeb.cs @@ -12,22 +12,20 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins { internal class IISWeb : IInstallationPlugin { - private readonly Target _target; private readonly ILogService _log; private readonly IIISClient _iisClient; private readonly IISWebOptions _options; private readonly IUserRoleService _userRoleService; - public IISWeb(Target target, IISWebOptions options, IIISClient iisClient, ILogService log, IUserRoleService userRoleService) + public IISWeb(IISWebOptions options, IIISClient iisClient, ILogService log, IUserRoleService userRoleService) { _iisClient = iisClient; _log = log; _options = options; - _target = target; _userRoleService = userRoleService; } - Task IInstallationPlugin.Install(IEnumerable<IStorePlugin> stores, CertificateInfo newCertificate, CertificateInfo? oldCertificate) + Task IInstallationPlugin.Install(Target target, IEnumerable<IStorePlugin> stores, CertificateInfo newCertificate, CertificateInfo? oldCertificate) { var bindingOptions = new BindingOptions(). WithThumbprint(newCertificate.Certificate.GetCertHash()); @@ -73,7 +71,7 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins } var oldThumb = oldCertificate?.Certificate?.GetCertHash(); - foreach (var part in _target.Parts) + foreach (var part in target.Parts) { _iisClient.AddOrUpdateBindings( part.Identifiers, diff --git a/src/main.lib/Plugins/InstallationPlugins/Script/Script.cs b/src/main.lib/Plugins/InstallationPlugins/Script/Script.cs index 7b5b876..3bf613f 100644 --- a/src/main.lib/Plugins/InstallationPlugins/Script/Script.cs +++ b/src/main.lib/Plugins/InstallationPlugins/Script/Script.cs @@ -20,7 +20,7 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins _client = client; } - public async Task Install(IEnumerable<IStorePlugin> store, CertificateInfo newCertificate, CertificateInfo? oldCertificate) + public async Task Install(Target target, IEnumerable<IStorePlugin> store, CertificateInfo newCertificate, CertificateInfo? oldCertificate) { if (_options.Script != null) { diff --git a/src/main.lib/Plugins/Interfaces/ICore.cs b/src/main.lib/Plugins/Interfaces/ICore.cs index 3b90ff0..a3a700b 100644 --- a/src/main.lib/Plugins/Interfaces/ICore.cs +++ b/src/main.lib/Plugins/Interfaces/ICore.cs @@ -1,4 +1,7 @@ -using System; +using PKISharp.WACS.Services; +using PKISharp.WACS.Services.Serialization; +using System; +using System.Threading.Tasks; namespace PKISharp.WACS.Plugins.Interfaces { @@ -43,6 +46,21 @@ namespace PKISharp.WACS.Plugins.Interfaces (bool, string?) Disabled { get; } } + public interface IPluginOptionsFactory<T>: IPluginOptionsFactory where T: PluginOptions + { + /// <summary> + /// Check or get configuration information needed (interactive) + /// </summary> + /// <param name="target"></param> + Task<T?> Aquire(IInputService inputService, RunLevel runLevel); + + /// <summary> + /// Check information needed (unattended) + /// </summary> + /// <param name="target"></param> + Task<T?> Default(); + } + public interface INull { } public interface IIgnore { } diff --git a/src/main.lib/Plugins/Interfaces/ICsrPluginOptionsFactory.cs b/src/main.lib/Plugins/Interfaces/ICsrPluginOptionsFactory.cs index af45be2..39005c1 100644 --- a/src/main.lib/Plugins/Interfaces/ICsrPluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Interfaces/ICsrPluginOptionsFactory.cs @@ -1,21 +1,6 @@ using PKISharp.WACS.Plugins.Base.Options; -using PKISharp.WACS.Services; -using System.Threading.Tasks; namespace PKISharp.WACS.Plugins.Interfaces { - public interface ICsrPluginOptionsFactory : IPluginOptionsFactory - { - /// <summary> - /// Check or get information needed for store (interactive) - /// </summary> - /// <param name="target"></param> - Task<CsrPluginOptions?> Aquire(IInputService inputService, RunLevel runLevel); - - /// <summary> - /// Check information needed for store (unattended) - /// </summary> - /// <param name="target"></param> - Task<CsrPluginOptions?> Default(); - } + public interface ICsrPluginOptionsFactory : IPluginOptionsFactory<CsrPluginOptions> {} } diff --git a/src/main.lib/Plugins/Interfaces/IInstallationPlugin.cs b/src/main.lib/Plugins/Interfaces/IInstallationPlugin.cs index 0732b06..b4272b5 100644 --- a/src/main.lib/Plugins/Interfaces/IInstallationPlugin.cs +++ b/src/main.lib/Plugins/Interfaces/IInstallationPlugin.cs @@ -16,6 +16,6 @@ namespace PKISharp.WACS.Plugins.Interfaces /// <param name="renewal"></param> /// <param name="newCertificateInfo"></param> /// <param name="oldCertificateInfo"></param> - Task Install(IEnumerable<IStorePlugin> stores, CertificateInfo newCertificateInfo, CertificateInfo? oldCertificateInfo); + Task Install(Target target, IEnumerable<IStorePlugin> stores, CertificateInfo newCertificateInfo, CertificateInfo? oldCertificateInfo); } } diff --git a/src/main.lib/Plugins/Interfaces/IOrderPlugin.cs b/src/main.lib/Plugins/Interfaces/IOrderPlugin.cs new file mode 100644 index 0000000..4b59586 --- /dev/null +++ b/src/main.lib/Plugins/Interfaces/IOrderPlugin.cs @@ -0,0 +1,10 @@ +using PKISharp.WACS.DomainObjects; +using System.Collections.Generic; + +namespace PKISharp.WACS.Plugins.Interfaces +{ + public interface IOrderPlugin + { + IEnumerable<Order> Split(Renewal renewal, Target target); + } +} diff --git a/src/main.lib/Plugins/Interfaces/IOrderPluginOptionsFactory.cs b/src/main.lib/Plugins/Interfaces/IOrderPluginOptionsFactory.cs new file mode 100644 index 0000000..446b2b2 --- /dev/null +++ b/src/main.lib/Plugins/Interfaces/IOrderPluginOptionsFactory.cs @@ -0,0 +1,16 @@ +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Plugins.Base.Options; + +namespace PKISharp.WACS.Plugins.Interfaces +{ + public interface IOrderPluginOptionsFactory : IPluginOptionsFactory<OrderPluginOptions> + { + /// <summary> + /// Is the order splitting option available for a specific target? + /// Used to rule out unfit orders + /// </summary> + /// <param name="target"></param> + /// <returns></returns> + bool CanProcess(Target target); + } +} diff --git a/src/main.lib/Plugins/Interfaces/IResolver.cs b/src/main.lib/Plugins/Interfaces/IResolver.cs index 85315ab..6a25448 100644 --- a/src/main.lib/Plugins/Interfaces/IResolver.cs +++ b/src/main.lib/Plugins/Interfaces/IResolver.cs @@ -8,18 +8,22 @@ namespace PKISharp.WACS.Plugins.Interfaces { public interface IResolver { + Task<ITargetPluginOptionsFactory> GetTargetPlugin(ILifetimeScope scope); + + Task<IValidationPluginOptionsFactory> GetValidationPlugin(ILifetimeScope scope, Target target); + + Task<IOrderPluginOptionsFactory> GetOrderPlugin(ILifetimeScope scope, Target target); + + Task<ICsrPluginOptionsFactory> GetCsrPlugin(ILifetimeScope scope); + + Task<IStorePluginOptionsFactory?> GetStorePlugin(ILifetimeScope scope, IEnumerable<IStorePluginOptionsFactory> chosen); + Task<IInstallationPluginOptionsFactory?> GetInstallationPlugin( ILifetimeScope scope, IEnumerable<Type> storeType, IEnumerable<IInstallationPluginOptionsFactory> chosen); - Task<IStorePluginOptionsFactory?> GetStorePlugin(ILifetimeScope scope, - IEnumerable<IStorePluginOptionsFactory> chosen); - Task<ITargetPluginOptionsFactory> GetTargetPlugin(ILifetimeScope scope); - Task<ICsrPluginOptionsFactory> GetCsrPlugin(ILifetimeScope scope); - - Task<IValidationPluginOptionsFactory> GetValidationPlugin(ILifetimeScope scope, Target target); } }
\ No newline at end of file diff --git a/src/main.lib/Plugins/Interfaces/IStorePluginOptionsFactory.cs b/src/main.lib/Plugins/Interfaces/IStorePluginOptionsFactory.cs index 3becd42..26c93c7 100644 --- a/src/main.lib/Plugins/Interfaces/IStorePluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Interfaces/IStorePluginOptionsFactory.cs @@ -1,24 +1,9 @@ using PKISharp.WACS.Plugins.Base.Options; -using PKISharp.WACS.Services; -using System.Threading.Tasks; namespace PKISharp.WACS.Plugins.Interfaces { /// <summary> /// StorePluginFactory interface /// </summary> - public interface IStorePluginOptionsFactory : IPluginOptionsFactory - { - /// <summary> - /// Check or get information needed for store (interactive) - /// </summary> - /// <param name="target"></param> - Task<StorePluginOptions?> Aquire(IInputService inputService, RunLevel runLevel); - - /// <summary> - /// Check information needed for store (unattended) - /// </summary> - /// <param name="target"></param> - Task<StorePluginOptions?> Default(); - } + public interface IStorePluginOptionsFactory : IPluginOptionsFactory<StorePluginOptions> { } } diff --git a/src/main.lib/Plugins/Interfaces/ITargetPluginOptionsFactory.cs b/src/main.lib/Plugins/Interfaces/ITargetPluginOptionsFactory.cs index d9b13d5..bff5c1e 100644 --- a/src/main.lib/Plugins/Interfaces/ITargetPluginOptionsFactory.cs +++ b/src/main.lib/Plugins/Interfaces/ITargetPluginOptionsFactory.cs @@ -1,28 +1,9 @@ using PKISharp.WACS.Plugins.Base.Options; -using PKISharp.WACS.Services; -using System.Threading.Tasks; namespace PKISharp.WACS.Plugins.Interfaces { /// <summary> /// TargetPluginFactory interface /// </summary> - public interface ITargetPluginOptionsFactory : IPluginOptionsFactory - { - /// <summary> - /// Hide when it cannot be chosen - /// </summary> - bool Hidden { get; } - /// <summary> - /// Check or get information needed for target (interactive) - /// </summary> - /// <param name="target"></param> - Task<TargetPluginOptions?> Aquire(IInputService inputService, RunLevel runLevel); - - /// <summary> - /// Check information needed for target (unattended) - /// </summary> - /// <param name="target"></param> - Task<TargetPluginOptions?> Default(); - } + public interface ITargetPluginOptionsFactory : IPluginOptionsFactory<TargetPluginOptions> { } } diff --git a/src/main.lib/Plugins/OrderPlugins/Host/Host.cs b/src/main.lib/Plugins/OrderPlugins/Host/Host.cs new file mode 100644 index 0000000..7cc3f29 --- /dev/null +++ b/src/main.lib/Plugins/OrderPlugins/Host/Host.cs @@ -0,0 +1,39 @@ +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Extensions; +using PKISharp.WACS.Plugins.Interfaces; +using System.Collections.Generic; +using System.Linq; + +namespace PKISharp.WACS.Plugins.OrderPlugins +{ + class Host : IOrderPlugin + { + public IEnumerable<Order> Split(Renewal renewal, Target target) + { + var ret = new List<Order>(); + var seen = new List<string>(); + foreach (var part in target.Parts) + { + foreach (var host in part.GetHosts(true)) + { + if (!seen.Contains(host)) + { + var parts = target.Parts.Where(p => p.GetHosts(true).Contains(host)); + var newTarget = new Target( + target.FriendlyName ?? "", + host, + parts.Select(p => new TargetPart(new List<string> { host }) { SiteId = p.SiteId })); + var newOrder = new Order( + renewal, + newTarget, + friendlyNamePart: host, + cacheKeyPart: $"{host}|{part.SiteId ?? -1}"); + ret.Add(newOrder); + seen.Add(host); + } + } + } + return ret; + } + } +} diff --git a/src/main.lib/Plugins/OrderPlugins/Host/HostOptions.cs b/src/main.lib/Plugins/OrderPlugins/Host/HostOptions.cs new file mode 100644 index 0000000..f68a887 --- /dev/null +++ b/src/main.lib/Plugins/OrderPlugins/Host/HostOptions.cs @@ -0,0 +1,12 @@ +using PKISharp.WACS.Plugins.Base; +using PKISharp.WACS.Plugins.Base.Options; + +namespace PKISharp.WACS.Plugins.OrderPlugins +{ + [Plugin("874a86e4-29c7-4294-9ab6-6908866847a0")] + internal class HostOptions : OrderPluginOptions<Host> + { + public override string Name => "Host"; + public override string Description => "Seperate certificate for each host"; + } +} diff --git a/src/main.lib/Plugins/OrderPlugins/Host/HostOptionsFactory.cs b/src/main.lib/Plugins/OrderPlugins/Host/HostOptionsFactory.cs new file mode 100644 index 0000000..719f1eb --- /dev/null +++ b/src/main.lib/Plugins/OrderPlugins/Host/HostOptionsFactory.cs @@ -0,0 +1,14 @@ +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Plugins.Base.Factories; +using PKISharp.WACS.Services; +using System.Threading.Tasks; + +namespace PKISharp.WACS.Plugins.OrderPlugins +{ + class HostOptionsFactory : OrderPluginOptionsFactory<Host, HostOptions> + { + public override bool CanProcess(Target target) => true; + public override Task<HostOptions> Aquire(IInputService inputService, RunLevel runLevel) => Default(); + public override Task<HostOptions> Default() => Task.FromResult(new HostOptions()); + } +} diff --git a/src/main.lib/Plugins/OrderPlugins/Single/Single.cs b/src/main.lib/Plugins/OrderPlugins/Single/Single.cs new file mode 100644 index 0000000..7c911e7 --- /dev/null +++ b/src/main.lib/Plugins/OrderPlugins/Single/Single.cs @@ -0,0 +1,11 @@ +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Plugins.Interfaces; +using System.Collections.Generic; + +namespace PKISharp.WACS.Plugins.OrderPlugins +{ + class Single : IOrderPlugin + { + public IEnumerable<Order> Split(Renewal renewal, Target target) => new List<Order>() { new Order(renewal, target) }; + } +} diff --git a/src/main.lib/Plugins/OrderPlugins/Single/SingleOptions.cs b/src/main.lib/Plugins/OrderPlugins/Single/SingleOptions.cs new file mode 100644 index 0000000..a4fcacf --- /dev/null +++ b/src/main.lib/Plugins/OrderPlugins/Single/SingleOptions.cs @@ -0,0 +1,12 @@ +using PKISharp.WACS.Plugins.Base; +using PKISharp.WACS.Plugins.Base.Options; + +namespace PKISharp.WACS.Plugins.OrderPlugins +{ + [Plugin("b705fa7c-1152-4436-8913-e433d7f84c82")] + internal class SingleOptions : OrderPluginOptions<Single> + { + public override string Name => "Single"; + public override string Description => "Single certificate"; + } +} diff --git a/src/main.lib/Plugins/OrderPlugins/Single/SingleOptionsFactory.cs b/src/main.lib/Plugins/OrderPlugins/Single/SingleOptionsFactory.cs new file mode 100644 index 0000000..05021c7 --- /dev/null +++ b/src/main.lib/Plugins/OrderPlugins/Single/SingleOptionsFactory.cs @@ -0,0 +1,14 @@ +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Plugins.Base.Factories; +using PKISharp.WACS.Services; +using System.Threading.Tasks; + +namespace PKISharp.WACS.Plugins.OrderPlugins +{ + class SingleOptionsFactory : OrderPluginOptionsFactory<Single, SingleOptions> + { + public override bool CanProcess(Target target) => true; + public override Task<SingleOptions> Aquire(IInputService inputService, RunLevel runLevel) => Default(); + public override Task<SingleOptions> Default() => Task.FromResult(new SingleOptions()); + } +} diff --git a/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs b/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs index 90c3254..2c682ec 100644 --- a/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs +++ b/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs @@ -1,5 +1,6 @@ using Autofac; using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Extensions; using PKISharp.WACS.Plugins.Base.Factories.Null; using PKISharp.WACS.Plugins.CsrPlugins; using PKISharp.WACS.Plugins.InstallationPlugins; @@ -25,10 +26,11 @@ namespace PKISharp.WACS.Plugins.Resolvers public InteractiveResolver( ILogService log, IInputService inputService, + ISettingsService settings, IArgumentsService arguments, IPluginService pluginService, RunLevel runLevel) - : base(log, arguments, pluginService) + : base(log, settings, arguments, pluginService) { _log = log; _input = inputService; @@ -36,45 +38,144 @@ namespace PKISharp.WACS.Plugins.Resolvers _runLevel = runLevel; } - /// <summary> - /// Allow user to choose a TargetPlugin - /// </summary> - /// <returns></returns> - public override async Task<ITargetPluginOptionsFactory> GetTargetPlugin(ILifetimeScope scope) + private async Task<T> GetPlugin<T>( + ILifetimeScope scope, + Type defaultType, + Type defaultTypeFallback, + T nullResult, + string className, + string shortDescription, + string longDescription, + string? defaultParam1 = null, + string? defaultParam2 = null, + Func<IEnumerable<T>, IEnumerable<T>>? sort = null, + Func<IEnumerable<T>, IEnumerable<T>>? filter = null, + Func<T, (bool, string?)>? unusable = null, + Func<T, string>? description = null, + bool allowAbort = true) where T : IPluginOptionsFactory { - var options = _plugins.TargetPluginFactories(scope). - Where(x => !x.Hidden). - OrderBy(x => x.Order). - ThenBy(x => x.Description); + // Helper method to determine final usability state + // combination of plugin being enabled (e.g. due to missing + // administrator rights) and being a right fit for the current + // renewal (e.g. cannot validate wildcards using http-01) + (bool, string?) disabledOrUnusable(T plugin) + { + var disabled = plugin.Disabled; + if (disabled.Item1) + { + return disabled; + } + else if (unusable != null) + { + return unusable(plugin); + } + return (false, null); + }; + + // Apply default sorting when no sorting has been provided yet + var options = _plugins.GetFactories<T>(scope); + options = filter != null ? filter(options) : options.Where(x => !(x is INull)); + options = sort != null ? sort(options) : options.OrderBy(x => x.Order).ThenBy(x => x.Description); - var defaultType = typeof(IISOptionsFactory); - if (!options.OfType<IISOptionsFactory>().Any(x => !x.Disabled.Item1)) + var localOptions = options. + Select(x => new { + plugin = x, + type = x.GetType(), + disabled = disabledOrUnusable(x) + }); + + // Default out when there are no reasonable options to pick + if (!localOptions.Any() || + localOptions.All(x => x.disabled.Item1) || + localOptions.All(x => x.plugin is INull)) { - defaultType = typeof(ManualOptionsFactory); + return nullResult; } - if (!_runLevel.HasFlag(RunLevel.Advanced)) + // Always show the menu in advanced mode, only when no default + // selection can be made in simple mode + var showMenu = _runLevel.HasFlag(RunLevel.Advanced); + if (!string.IsNullOrEmpty(defaultParam1)) { - return (ITargetPluginOptionsFactory)scope.Resolve(defaultType); + var defaultPlugin = _plugins.GetFactory<T>(scope, defaultParam1, defaultParam2); + if (defaultPlugin == null) + { + _log.Error("Unable to find {n} plugin {p}", className, defaultParam1); + showMenu = true; + } + else + { + defaultType = defaultPlugin.GetType(); + } + } + + var defaultOption = localOptions.First(x => x.type == defaultType); + var defaultTypeDisabled = defaultOption.disabled; + if (defaultTypeDisabled.Item1) + { + _log.Warning("{n} plugin {x} not available: {m}", + char.ToUpper(className[0]) + className.Substring(1), + defaultOption.plugin.Name, + defaultTypeDisabled.Item2); + defaultType = defaultTypeFallback; + showMenu = true; + } + + if (!showMenu) + { + return (T)scope.Resolve(defaultType); } // List options for generating new certificates - _input.Show(null, "Please specify how the list of domain names that will be included in the certificate " + - "should be determined. If you choose for one of the \"all bindings\" options, the list will automatically be " + - "updated for future renewals to reflect the bindings at that time.", - true); + if (!string.IsNullOrEmpty(longDescription)) + { + _input.Show(null, longDescription, true); + } - var ret = await _input.ChooseOptional( - "How shall we determine the domain(s) to include in the certificate?", - options, - x => Choice.Create<ITargetPluginOptionsFactory?>( - x, - description: x.Description, - @default: x.GetType() == defaultType, - disabled: x.Disabled), - "Abort"); + Choice<IPluginOptionsFactory?> creator(T plugin, Type type, (bool, string?) disabled) { + return Choice.Create<IPluginOptionsFactory?>( + plugin, + description: description == null ? plugin.Description : description(plugin), + @default: type == defaultType && !disabled.Item1, + disabled: disabled); + } - return ret ?? new NullTargetFactory(); + var ret = default(T); + if (allowAbort) + { + ret = (T)await _input.ChooseOptional( + shortDescription, + localOptions, + x => creator(x.plugin, x.type, x.disabled), + "Abort"); + } + else + { + ret = (T)await _input.ChooseRequired( + shortDescription, + localOptions, + x => creator(x.plugin, x.type, x.disabled)); + } + return ret ?? nullResult; + } + + /// <summary> + /// Allow user to choose a TargetPlugin + /// </summary> + /// <returns></returns> + public override async Task<ITargetPluginOptionsFactory> GetTargetPlugin(ILifetimeScope scope) + { + return await GetPlugin<ITargetPluginOptionsFactory>( + scope, + defaultParam1: _settings.Target.DefaultTarget, + defaultType: typeof(IISOptionsFactory), + defaultTypeFallback: typeof(ManualOptionsFactory), + nullResult: new NullTargetFactory(), + className: "target", + shortDescription: "How shall we determine the domain(s) to include in the certificate?", + longDescription: "Please specify how the list of domain names that will be included in the certificate " + + "should be determined. If you choose for one of the \"all bindings\" options, the list will automatically be " + + "updated for future renewals to reflect the bindings at that time."); } /// <summary> @@ -83,19 +184,12 @@ namespace PKISharp.WACS.Plugins.Resolvers /// <returns></returns> public override async Task<IValidationPluginOptionsFactory> GetValidationPlugin(ILifetimeScope scope, Target target) { - if (_runLevel.HasFlag(RunLevel.Advanced)) - { - // List options for generating new certificates - _input.Show(null, "The ACME server will need to verify that you are the owner of the domain names that you are requesting" + - " the certificate for. This happens both during initial setup *and* for every future renewal. There are two main methods of doing so: " + - "answering specific http requests (http-01) or create specific dns records (dns-01). For wildcard domains the latter is the only option. " + - "Various additional plugins are available from https://github.com/win-acme/win-acme/.", - true); - - var options = _plugins.ValidationPluginFactories(scope). - Where(x => !(x is INull)). - Where(x => x.CanValidate(target)). - OrderBy(x => { + return await GetPlugin<IValidationPluginOptionsFactory>( + scope, + sort: x => + x. + OrderBy(x => + { return x.ChallengeType switch { Constants.Http01ChallengeType => 0, @@ -105,125 +199,71 @@ namespace PKISharp.WACS.Plugins.Resolvers }; }). ThenBy(x => x.Order). - ThenBy(x => x.Description); - - var defaultType = typeof(SelfHostingOptionsFactory); - if (!options.OfType<SelfHostingOptionsFactory>().Any(x => !x.Disabled.Item1)) - { - defaultType = typeof(FileSystemOptionsFactory); - } - var ret = await _input.ChooseOptional( - "How would you like prove ownership for the domain(s) in the certificate?", - options, - x => Choice.Create<IValidationPluginOptionsFactory?>( - x, - description: $"[{x.ChallengeType}] {x.Description}", - @default: x.GetType() == defaultType, - disabled: x.Disabled), - "Abort"); - return ret ?? new NullValidationFactory(); - } - else - { - var ret = scope.Resolve<SelfHostingOptionsFactory>(); - if (ret.CanValidate(target)) - { - return ret; - } - else - { - _log.Error("The default validation plugin cannot be " + - "used for this target. Most likely this is because " + - "you have included a wildcard identifier (*.example.com), " + - "which requires DNS validation. Choose another plugin " + - "from the advanced menu ('M')."); - return new NullValidationFactory(); - } - } + ThenBy(x => x.Description), + unusable: x => (!x.CanValidate(target), "Unsuppored target. Most likely this is because you have included a wildcard identifier (*.example.com), which requires DNS validation."), + description: x => $"[{x.ChallengeType}] {x.Description}", + defaultParam1: _settings.Validation.DefaultValidation, + defaultParam2: _settings.Validation.DefaultValidationMode ?? Constants.Http01ChallengeType, + defaultType: typeof(SelfHostingOptionsFactory), + defaultTypeFallback: typeof(FileSystemOptionsFactory), + nullResult: new NullValidationFactory(), + className: "validation", + shortDescription: "How would you like prove ownership for the domain(s)?", + longDescription: "The ACME server will need to verify that you are the owner of the domain names that you are requesting" + + " the certificate for. This happens both during initial setup *and* for every future renewal. There are two main methods of doing so: " + + "answering specific http requests (http-01) or create specific dns records (dns-01). For wildcard domains the latter is the only option. " + + "Various additional plugins are available from https://github.com/win-acme/win-acme/."); } public override async Task<ICsrPluginOptionsFactory> GetCsrPlugin(ILifetimeScope scope) { - if (string.IsNullOrEmpty(_options.MainArguments.Csr) && - _runLevel.HasFlag(RunLevel.Advanced)) - { - _input.Show(null, "After ownership of the domain(s) has been proven, we will create" + - " a Certificate Signing Request (CSR) to obtain the actual certificate. " + - "The CSR determines properties of the certificate like which " + - "(type of) key to use. If you are not sure what to pick here, RSA is the safe default.", - true); - - var ret = await _input.ChooseRequired( - "What kind of private key should be used for the certificate?", - _plugins.CsrPluginOptionsFactories(scope). - Where(x => !(x is INull)). - OrderBy(x => x.Order). - ThenBy(x => x.Description), - x => Choice.Create( - x, - description: x.Description, - @default: x is RsaOptionsFactory, - disabled: x.Disabled)); - return ret; - } - else - { - return await base.GetCsrPlugin(scope); - } + return await GetPlugin<ICsrPluginOptionsFactory>( + scope, + defaultParam1: _settings.Csr.DefaultCsr, + defaultType: typeof(RsaOptionsFactory), + defaultTypeFallback: typeof(EcOptionsFactory), + nullResult: new NullCsrFactory(), + className: "csr", + shortDescription: "What kind of private key should be used for the certificate?", + longDescription: "After ownership of the domain(s) has been proven, we will create a " + + "Certificate Signing Request (CSR) to obtain the actual certificate. The CSR " + + "determines properties of the certificate like which (type of) key to use. If you " + + "are not sure what to pick here, RSA is the safe default."); } public override async Task<IStorePluginOptionsFactory?> GetStorePlugin(ILifetimeScope scope, IEnumerable<IStorePluginOptionsFactory> chosen) { - if (string.IsNullOrEmpty(_options.MainArguments.Store) && _runLevel.HasFlag(RunLevel.Advanced)) + var defaultType = typeof(CertificateStoreOptionsFactory); + var shortDescription = "How would you like to store the certificate?"; + var longDescription = "When we have the certificate, you can store in one or more ways to make it accessible " + + "to your applications. The Windows Certificate Store is the default location for IIS (unless you are " + + "managing a cluster of them)."; + if (chosen.Count() != 0) { - var filtered = _plugins. - StorePluginFactories(scope). - Except(chosen). - OrderBy(x => x.Order). - ThenBy(x => x.Description). - ToList(); - - if (filtered.Where(x => !x.Disabled.Item1).Count() == 0) + if (!_runLevel.HasFlag(RunLevel.Advanced)) { return new NullStoreOptionsFactory(); } - - if (chosen.Count() == 0) - { - _input.Show(null, "When we have the certificate, you can store in one or more ways to make it accessible " + - "to your applications. The Windows Certificate Store is the default location for IIS (unless you are " + - "managing a cluster of them).", - true); - } - var question = "How would you like to store the certificate?"; - var defaultType = typeof(CertificateStoreOptionsFactory); - if (!filtered.OfType<CertificateStoreOptionsFactory>().Any(x => !x.Disabled.Item1)) - { - defaultType = typeof(PemFilesOptionsFactory); - } - - if (chosen.Count() != 0) - { - question = "Would you like to store it in another way too?"; - defaultType = typeof(NullStoreOptionsFactory); - } - - var store = await _input.ChooseOptional( - question, - filtered, - x => Choice.Create<IStorePluginOptionsFactory?>( - x, - description: x.Description, - @default: x.GetType() == defaultType, - disabled: x.Disabled), - "Abort"); - - return store; - } - else - { - return await base.GetStorePlugin(scope, chosen); + longDescription = ""; + shortDescription = "Would you like to store it in another way too?"; + defaultType = typeof(NullStoreOptionsFactory); } + var defaultParam1 = _settings.Store.DefaultStore; + var csv = defaultParam1.ParseCsv(); + defaultParam1 = csv?.Count > chosen.Count() ? + csv[chosen.Count()] : + ""; + return await GetPlugin<IStorePluginOptionsFactory>( + scope, + filter: (x) => x.Except(chosen), + defaultParam1: defaultParam1, + defaultType: defaultType, + defaultTypeFallback: typeof(PemFilesOptionsFactory), + nullResult: new NullStoreOptionsFactory(), + className: "store", + shortDescription: shortDescription, + longDescription: longDescription, + allowAbort: false); } /// <summary> @@ -235,69 +275,38 @@ namespace PKISharp.WACS.Plugins.Resolvers /// <returns></returns> public override async Task<IInstallationPluginOptionsFactory?> GetInstallationPlugin(ILifetimeScope scope, IEnumerable<Type> storeTypes, IEnumerable<IInstallationPluginOptionsFactory> chosen) { - if (_runLevel.HasFlag(RunLevel.Advanced)) - { - var filtered = _plugins. - InstallationPluginFactories(scope). - Except(chosen). - OrderBy(x => x.Order). - ThenBy(x => x.Description). - Select(x => new { - plugin = x, - usable = !x.Disabled.Item1 && x.CanInstall(storeTypes) - }). - ToList(); - - var usable = filtered.Where(x => x.usable); - if (usable.Count() == 0) - { - return new NullInstallationOptionsFactory(); - } - - if (usable.Count() == 1 && usable.First().plugin is NullInstallationOptionsFactory) - { - return new NullInstallationOptionsFactory(); - } - - if (chosen.Count() == 0) - { - _input.Show(null, "With the certificate saved to the store(s) of your choice, you may choose one or more steps to update your applications, e.g. to configure the new thumbprint, or to update bindings.", true); - } - - var question = "Which installation step should run first?"; - var @default = usable.Any(x => x.plugin is IISWebOptionsFactory) ? - typeof(IISWebOptionsFactory) : - typeof(NullInstallationOptionsFactory); - - if (chosen.Count() != 0) - { - question = "Add another installation step?"; - @default = typeof(NullInstallationOptionsFactory); - } - - var install = await _input.ChooseRequired( - question, - filtered, - x => Choice.Create( - x, - description: x.plugin.Description, - disabled: (!x.usable, x.plugin.Disabled.Item1 ? - x.plugin.Disabled.Item2 : "Incompatible with selected store."), - @default: x.plugin.GetType() == @default)) ; - - return install.plugin; - } - else + var defaultType = typeof(IISWebOptionsFactory); + var shortDescription = "Which installation step should run first?"; + var longDescription = "With the certificate saved to the store(s) of your choice, " + + "you may choose one or more steps to update your applications, e.g. to configure " + + "the new thumbprint, or to update bindings."; + if (chosen.Count() != 0) { - if (chosen.Count() == 0) - { - return scope.Resolve<IISWebOptionsFactory>(); - } - else + if (!_runLevel.HasFlag(RunLevel.Advanced)) { return new NullInstallationOptionsFactory(); } + longDescription = ""; + shortDescription = "Add another installation step?"; + defaultType = typeof(NullInstallationOptionsFactory); } + var defaultParam1 = _settings.Installation.DefaultInstallation; + var csv = defaultParam1.ParseCsv(); + defaultParam1 = csv?.Count > chosen.Count() ? + csv[chosen.Count()] : + ""; + return await GetPlugin<IInstallationPluginOptionsFactory>( + scope, + filter: (x) => x.Except(chosen), + unusable: x => (!x.CanInstall(storeTypes), "This step cannot be used in combination with the specified store(s)"), + defaultParam1: defaultParam1, + defaultType: defaultType, + defaultTypeFallback: typeof(NullInstallationOptionsFactory), + nullResult: new NullInstallationOptionsFactory(), + className: "installation", + shortDescription: shortDescription, + longDescription: longDescription, + allowAbort: false); } } } diff --git a/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs b/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs index f898d87..a4d0d09 100644 --- a/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs +++ b/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs @@ -4,7 +4,9 @@ using PKISharp.WACS.Extensions; using PKISharp.WACS.Plugins.Base.Factories.Null; using PKISharp.WACS.Plugins.CsrPlugins; using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Plugins.OrderPlugins; using PKISharp.WACS.Plugins.StorePlugins; +using PKISharp.WACS.Plugins.TargetPlugins; using PKISharp.WACS.Plugins.ValidationPlugins.Http; using PKISharp.WACS.Services; using System; @@ -17,116 +19,160 @@ namespace PKISharp.WACS.Plugins.Resolvers public class UnattendedResolver : IResolver { private readonly IPluginService _plugins; - protected IArgumentsService _options; + protected readonly IArgumentsService _arguments; + protected readonly ISettingsService _settings; private readonly ILogService _log; - public UnattendedResolver(ILogService log, IArgumentsService options, IPluginService pluginService) + public UnattendedResolver(ILogService log, ISettingsService settings, IArgumentsService arguments, IPluginService pluginService) { _log = log; _plugins = pluginService; - _options = options; + _arguments = arguments; + _settings = settings; } - /// <summary> - /// Get the TargetPlugin which was used (or can be assumed to have been used) to create this - /// ScheduledRenewal - /// </summary> - /// <returns></returns> - public virtual async Task<ITargetPluginOptionsFactory> GetTargetPlugin(ILifetimeScope scope) + private async Task<T> GetPlugin<T>( + ILifetimeScope scope, + Type defaultType, + T nullResult, + string className, + string? defaultParam1 = null, + string? defaultParam2 = null, + Func<IEnumerable<T>, IEnumerable<T>>? filter = null, + Func<T, (bool, string?)>? unusable = null) where T: IPluginOptionsFactory { - // Get plugin factory - if (string.IsNullOrEmpty(_options.MainArguments.Target)) + // Helper method to determine final usability state + // combination of plugin being enabled (e.g. due to missing + // administrator rights) and being a right fit for the current + // renewal (e.g. cannot validate wildcards using http-01) + (bool, string?) disabledOrUnusable(T plugin) + { + var disabled = (plugin as IPluginOptionsFactory)?.Disabled ?? (true, "Invalid plugin"); + if (disabled.Item1) + { + return disabled; + } + else if (unusable != null) + { + return unusable(plugin); + } + return (false, null); + }; + + // Apply default sorting when no sorting has been provided yet + var options = _plugins.GetFactories<T>(scope); + options = filter != null ? filter(options) : options.Where(x => !(x is INull)); + var localOptions = options. + Select(x => new { + plugin = x, + type = x?.GetType(), + disabled = disabledOrUnusable(x) + }); + + // Default out when there are no reasonable options to pick + if (!localOptions.Any() || + localOptions.All(x => x.disabled.Item1) || + localOptions.All(x => x.plugin is INull)) { - return new NullTargetFactory(); + return nullResult; } - var targetPluginFactory = _plugins.TargetPluginFactory(scope, _options.MainArguments.Target); - if (targetPluginFactory == null) + + var changeInstructions = $"Choose another plugin using the --{className} switch or change the default in settings.json"; + if (!string.IsNullOrEmpty(defaultParam1)) { - _log.Error("Unable to find target plugin {PluginName}", _options.MainArguments.Target); - return new NullTargetFactory(); + var defaultPlugin = _plugins.GetFactory<T>(scope, defaultParam1, defaultParam2); + if (defaultPlugin == null) + { + _log.Error("Unable to find {n} plugin {p}. " + changeInstructions, className, defaultParam1); + return nullResult; + } + else + { + defaultType = defaultPlugin.GetType(); + } } - var (disabled, disabledReason) = targetPluginFactory.Disabled; - if (disabled) + + var defaultOption = localOptions.First(x => x.type == defaultType); + var defaultTypeDisabled = defaultOption.disabled; + if (defaultTypeDisabled.Item1) { - _log.Error($"Target plugin {{PluginName}} is not available. {disabledReason}", _options.MainArguments.Target); - return new NullTargetFactory(); + _log.Error("{n} plugin {x} not available: {m}. " + changeInstructions, + char.ToUpper(className[0]) + className.Substring(1), + (defaultOption.plugin as IPluginOptionsFactory)?.Name ?? "Unknown", + defaultTypeDisabled.Item2); + return nullResult; } - return targetPluginFactory; + + return (T)scope.Resolve(defaultType); + } + + /// <summary> + /// Get the TargetPlugin which was used (or can be assumed to have been used) to create this + /// Renewal + /// </summary> + /// <returns></returns> + public virtual async Task<ITargetPluginOptionsFactory> GetTargetPlugin(ILifetimeScope scope) + { + // NOTE: checking the default option here doesn't make + // sense because MainArguments.Target is what triggers + // unattended mode in the first place. We woudn't even + // get into this code unless it was specified. + return await GetPlugin<ITargetPluginOptionsFactory>( + scope, + defaultParam1: _arguments.MainArguments.Target, + defaultType: typeof(ManualOptionsFactory), + nullResult: new NullTargetFactory(), + className: "target"); } /// <summary> /// Get the ValidationPlugin which was used (or can be assumed to have been used) - /// to validate this ScheduledRenewal + /// to validate this Renewal /// </summary> /// <returns></returns> public virtual async Task<IValidationPluginOptionsFactory> GetValidationPlugin(ILifetimeScope scope, Target target) { - // Get plugin factory - var validationPluginFactory = string.IsNullOrEmpty(_options.MainArguments.Validation) - ? scope.Resolve<SelfHostingOptionsFactory>() - : _plugins.ValidationPluginFactory(scope, - _options.MainArguments.ValidationMode ?? Constants.Http01ChallengeType, - _options.MainArguments.Validation); - - if (validationPluginFactory == null) - { - _log.Error("Unable to find validation plugin {PluginName}", _options.MainArguments.Validation); - return new NullValidationFactory(); - } - var (disabled, disabledReason) = validationPluginFactory.Disabled; - if (disabled) - { - _log.Error($"Validation plugin {{PluginName}} is not available. {disabledReason}", validationPluginFactory.Name); - return new NullValidationFactory(); - } - if (!validationPluginFactory.CanValidate(target)) - { - _log.Error("Validation plugin {PluginName} cannot validate this target", validationPluginFactory.Name); - return new NullValidationFactory(); - } - return validationPluginFactory; + return await GetPlugin<IValidationPluginOptionsFactory>( + scope, + defaultParam1: _arguments.MainArguments.Validation ?? + _settings.Validation.DefaultValidation, + defaultParam2: _arguments.MainArguments.ValidationMode ?? + _settings.Validation.DefaultValidationMode ?? + Constants.Http01ChallengeType, + defaultType: typeof(SelfHostingOptionsFactory), + nullResult: new NullValidationFactory(), + unusable: (c) => (!c.CanValidate(target), "Unsuppored target. Most likely this is because you have included a wildcard identifier (*.example.com), which requires DNS validation."), + className: "validation"); } /// <summary> - /// Get the InstallationPlugin which was used (or can be assumed to have been used) to install - /// this ScheduledRenewal + /// Get the OrderPlugin which is used to convert the target into orders + /// and request the certificate /// </summary> /// <returns></returns> - public virtual async Task<IInstallationPluginOptionsFactory?> GetInstallationPlugin(ILifetimeScope scope, IEnumerable<Type> storeTypes, IEnumerable<IInstallationPluginOptionsFactory> chosen) + public virtual async Task<IOrderPluginOptionsFactory> GetOrderPlugin(ILifetimeScope scope, Target target) { - if (string.IsNullOrEmpty(_options.MainArguments.Installation)) - { - return new NullInstallationOptionsFactory(); - } - else - { - var parts = _options.MainArguments.Installation.ParseCsv(); - var index = chosen.Count(); - if (parts == null || index == parts.Count) - { - return new NullInstallationOptionsFactory(); - } + return await GetPlugin<IOrderPluginOptionsFactory>( + scope, + defaultParam1: _arguments.MainArguments.Order, + defaultType: typeof(SingleOptionsFactory), + nullResult: new NullOrderOptionsFactory(), + className: "order"); + } - var name = parts[index]; - var factory = _plugins.InstallationPluginFactory(scope, name); - if (factory == null) - { - _log.Error("Unable to find installation plugin {PluginName}", name); - return null; - } - var (disabled, disabledReason) = factory.Disabled; - if (disabled) - { - _log.Error($"Installation plugin {{PluginName}} is not available. {disabledReason}", name); - return null; - } - if (!factory.CanInstall(storeTypes)) - { - _log.Error("Installation plugin {PluginName} cannot install from selected store(s)", name); - return null; - } - return factory; - } + /// <summary> + /// Get the CsrPlugin which is used to generate the private key + /// and request the certificate + /// </summary> + /// <returns></returns> + public virtual async Task<ICsrPluginOptionsFactory> GetCsrPlugin(ILifetimeScope scope) + { + return await GetPlugin<ICsrPluginOptionsFactory>( + scope, + defaultParam1: _arguments.MainArguments.Csr, + defaultType: typeof(RsaOptionsFactory), + nullResult: new NullCsrFactory(), + className: "csr"); } /// <summary> @@ -135,72 +181,60 @@ namespace PKISharp.WACS.Plugins.Resolvers /// <returns></returns> public virtual async Task<IStorePluginOptionsFactory?> GetStorePlugin(ILifetimeScope scope, IEnumerable<IStorePluginOptionsFactory> chosen) { - var args = _options.MainArguments.Store; - if (string.IsNullOrEmpty(args)) + var cmd = _arguments.MainArguments.Store ?? _settings.Store.DefaultStore; + if (string.IsNullOrEmpty(cmd)) { - if (chosen.Count() == 0) - { - args = CertificateStoreOptions.PluginName; - } - else - { - return new NullStoreOptionsFactory(); - } + cmd = CertificateStoreOptions.PluginName; } - - var parts = args.ParseCsv(); + var parts = cmd.ParseCsv(); if (parts == null) { return null; } - var index = chosen.Count(); if (index == parts.Count) { return new NullStoreOptionsFactory(); } - - var name = parts[index]; - var factory = _plugins.StorePluginFactory(scope, name); - if (factory == null) - { - _log.Error("Unable to find store plugin {PluginName}", name); - return null; - } - var (disabled, disabledReason) = factory.Disabled; - if (disabled) - { - _log.Error($"Store plugin {{PluginName}} is not available. {disabledReason}", name); - return null; - } - return factory; + return await GetPlugin<IStorePluginOptionsFactory>( + scope, + filter: x => x, + defaultParam1: parts[index], + defaultType: typeof(NullStoreOptionsFactory), +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + nullResult: default, +#pragma warning restore CS8625 + className: "store"); } /// <summary> - /// Get the CsrPlugin which is used to generate the private key - /// and request the certificate + /// Get the InstallationPlugin which was used (or can be assumed to have been used) to install + /// this ScheduledRenewal /// </summary> /// <returns></returns> - public virtual async Task<ICsrPluginOptionsFactory> GetCsrPlugin(ILifetimeScope scope) + public virtual async Task<IInstallationPluginOptionsFactory?> GetInstallationPlugin(ILifetimeScope scope, IEnumerable<Type> storeTypes, IEnumerable<IInstallationPluginOptionsFactory> chosen) { - var pluginName = _options.MainArguments.Csr; - if (string.IsNullOrEmpty(pluginName)) - { - return scope.Resolve<RsaOptionsFactory>(); - } - var factory = _plugins.CsrPluginFactory(scope, pluginName); - if (factory == null) + var cmd = _arguments.MainArguments.Installation ?? _settings.Installation.DefaultInstallation; + var parts = cmd.ParseCsv(); + if (parts == null) { - _log.Error("Unable to find csr plugin {PluginName}", pluginName); - return new NullCsrFactory(); + return new NullInstallationOptionsFactory(); } - var (disabled, disabledReason) = factory.Disabled; - if (disabled) + var index = chosen.Count(); + if (index == parts.Count) { - _log.Error($"CSR plugin {{PluginName}} is not available. {disabledReason}", pluginName); - return new NullCsrFactory(); + return new NullInstallationOptionsFactory(); } - return factory; + return await GetPlugin<IInstallationPluginOptionsFactory>( + scope, + filter: x => x, + unusable: x => (!x.CanInstall(storeTypes), "This step cannot be used in combination with the specified store(s)"), + defaultParam1: parts[index], + defaultType: typeof(NullInstallationOptionsFactory), +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + nullResult: default, +#pragma warning restore CS8625 + className: "store"); } } } diff --git a/src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs b/src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs index 37a44db..a1afe1e 100644 --- a/src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs +++ b/src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs @@ -18,7 +18,6 @@ namespace PKISharp.WACS.Plugins.TargetPlugins public IISOptionsFactory( ILogService log, - IIISClient iisClient, IISHelper iisHelper, IArgumentsService arguments, IUserRoleService userRoleService) @@ -26,7 +25,6 @@ namespace PKISharp.WACS.Plugins.TargetPlugins _iisHelper = iisHelper; _log = log; _arguments = arguments; - Hidden = !(iisClient.Version.Major > 6); Disabled = IIS.Disabled(userRoleService); } @@ -196,7 +194,6 @@ namespace PKISharp.WACS.Plugins.TargetPlugins } // Exclude specific bindings - var listForCommon = false; if (askExclude && filtered.Count > 1 && runLevel.HasFlag(RunLevel.Advanced)) { await ListBindings(input, runLevel, filtered); @@ -209,23 +206,12 @@ namespace PKISharp.WACS.Plugins.TargetPlugins if (options.ExcludeHosts != null) { filtered = _iisHelper.FilterBindings(allBindings, options); - listForCommon = true; } } - else - { - listForCommon = true; - } // Now the common name if (filtered.Select(x => x.HostUnicode).Distinct().Count() > 1) { - // If no bindings have been excluded, we can re-use - // the previously printed list - if (listForCommon) - { - await ListBindings(input, runLevel, filtered); - } await InputCommonName(input, filtered, options); } return options; @@ -279,26 +265,13 @@ namespace PKISharp.WACS.Plugins.TargetPlugins async Task InputCommonName(IInputService input, List<IISHelper.IISBindingOption> filtered, IISOptions options) { var sorted = SortBindings(filtered).ToList(); - string raw; - do - { - input.Show(null, "Please pick the most important host name from the list. " + - "This will be displayed to your users as the subject of the certificate.", - true); - raw = await input.RequestString("Common name"); - if (!string.IsNullOrEmpty(raw)) - { - // Magically replace binding identifiers by their proper host names - if (int.TryParse(raw, out var id)) - { - if (id > 0 && id <= sorted.Count()) - { - raw = sorted[id - 1].HostUnicode; - } - } - } - } - while (!ParseCommonName(raw, filtered.Select(x => x.HostUnicode), options)); + var common = await input.ChooseRequired( + "Please pick the main host, which will be presented as the subject of the certificate", + sorted, + (x) => Choice.Create(x, + description: x.HostUnicode, + @default: sorted.IndexOf(x) == 0)); + ParseCommonName(common.HostUnicode, filtered.Select(x => x.HostUnicode), options); } /// <summary> diff --git a/src/main.lib/RenewalCreator.cs b/src/main.lib/RenewalCreator.cs index 69131e5..ed82881 100644 --- a/src/main.lib/RenewalCreator.cs +++ b/src/main.lib/RenewalCreator.cs @@ -60,7 +60,7 @@ namespace PKISharp.WACS // with the same --friendlyname in unattended mode. if (existing == null && string.IsNullOrEmpty(_args.Id)) { - existing = _renewalStore.FindByArguments(null, temp.LastFriendlyName).FirstOrDefault(); + existing = _renewalStore.FindByArguments(null, temp.LastFriendlyName?.EscapePattern()).FirstOrDefault(); } // This will be a completely new renewal, no further processing needed @@ -133,45 +133,41 @@ namespace PKISharp.WACS tempRenewal.TargetPluginOptions = targetPluginOptions; // Generate Target and validation plugin choice - Target? initialTarget = null; - IValidationPluginOptionsFactory? validationPluginOptionsFactory = null; - using (var targetScope = _scopeBuilder.Target(_container, tempRenewal, runLevel)) + using var targetScope = _scopeBuilder.Target(_container, tempRenewal, runLevel); + var initialTarget = targetScope.Resolve<Target>(); + if (initialTarget is INull) { - initialTarget = targetScope.Resolve<Target>(); - if (initialTarget is INull) - { - _exceptionHandler.HandleException(message: $"Target plugin {targetPluginOptionsFactory.Name} was unable to generate a target"); - return; - } - if (!initialTarget.IsValid(_log)) - { - _exceptionHandler.HandleException(message: $"Target plugin {targetPluginOptionsFactory.Name} generated an invalid target"); - return; - } - _log.Information("Target generated using plugin {name}: {target}", targetPluginOptions.Name, initialTarget); + _exceptionHandler.HandleException(message: $"Target plugin {targetPluginOptionsFactory.Name} was unable to generate a target"); + return; + } + if (!initialTarget.IsValid(_log)) + { + _exceptionHandler.HandleException(message: $"Target plugin {targetPluginOptionsFactory.Name} generated an invalid target"); + return; + } + _log.Information("Target generated using plugin {name}: {target}", targetPluginOptions.Name, initialTarget); - // Choose FriendlyName - if (!string.IsNullOrEmpty(_args.FriendlyName)) - { - tempRenewal.FriendlyName = _args.FriendlyName; - } - else if (runLevel.HasFlag(RunLevel.Advanced | RunLevel.Interactive)) + // Choose FriendlyName + if (!string.IsNullOrEmpty(_args.FriendlyName)) + { + tempRenewal.FriendlyName = _args.FriendlyName; + } + else if (runLevel.HasFlag(RunLevel.Advanced | RunLevel.Interactive)) + { + var alt = await _input.RequestString($"Suggested friendly name '{initialTarget.FriendlyName}', press <ENTER> to accept or type an alternative"); + if (!string.IsNullOrEmpty(alt)) { - var alt = await _input.RequestString($"Suggested friendly name '{initialTarget.FriendlyName}', press <ENTER> to accept or type an alternative"); - if (!string.IsNullOrEmpty(alt)) - { - tempRenewal.FriendlyName = alt; - } + tempRenewal.FriendlyName = alt; } - tempRenewal.LastFriendlyName = tempRenewal.FriendlyName ?? initialTarget.FriendlyName; + } + tempRenewal.LastFriendlyName = tempRenewal.FriendlyName ?? initialTarget.FriendlyName; - // Choose validation plugin - validationPluginOptionsFactory = targetScope.Resolve<IValidationPluginOptionsFactory>(); - if (validationPluginOptionsFactory is INull) - { - _exceptionHandler.HandleException(message: $"No validation plugin could be selected"); - return; - } + // Choose validation plugin + var validationPluginOptionsFactory = targetScope.Resolve<IValidationPluginOptionsFactory>(); + if (validationPluginOptionsFactory is INull) + { + _exceptionHandler.HandleException(message: $"No validation plugin could be selected"); + return; } // Configure validation @@ -193,6 +189,33 @@ namespace PKISharp.WACS return; } + // Choose order plugin + var orderPluginOptionsFactory = targetScope.Resolve<IOrderPluginOptionsFactory>(); + if (orderPluginOptionsFactory is INull) + { + _exceptionHandler.HandleException(message: $"No order plugin could be selected"); + return; + } + + // Configure order + try + { + var orderOptions = runLevel.HasFlag(RunLevel.Unattended) ? + await orderPluginOptionsFactory.Default() : + await orderPluginOptionsFactory.Aquire(_input, runLevel); + if (orderOptions == null) + { + _exceptionHandler.HandleException(message: $"Order plugin {orderPluginOptionsFactory.Name} was unable to generate options"); + return; + } + tempRenewal.OrderPluginOptions = orderOptions; + } + catch (Exception ex) + { + _exceptionHandler.HandleException(ex, $"Order plugin {orderPluginOptionsFactory.Name} aborted or failed"); + return; + } + // Choose CSR plugin if (initialTarget.CsrBytes == null) { @@ -324,8 +347,8 @@ namespace PKISharp.WACS // Try to run for the first time var renewal = await CreateRenewal(tempRenewal, runLevel); retry: - var result = await _renewalExecution.Execute(renewal, runLevel); - if (result == null) + var result = await _renewalExecution.HandleRenewal(renewal, runLevel); + if (result.Abort) { _exceptionHandler.HandleException(message: $"Create certificate cancelled"); } @@ -336,7 +359,7 @@ namespace PKISharp.WACS { goto retry; } - _exceptionHandler.HandleException(message: $"Create certificate failed: {result?.ErrorMessage}"); + _exceptionHandler.HandleException(message: $"Create certificate failed: {string.Join(", ", result.ErrorMessages)}"); } else { diff --git a/src/main.lib/RenewalExecutor.cs b/src/main.lib/RenewalExecutor.cs index a4a8eb3..4dbedbe 100644 --- a/src/main.lib/RenewalExecutor.cs +++ b/src/main.lib/RenewalExecutor.cs @@ -1,6 +1,4 @@ -using ACMESharp.Protocol; -using ACMESharp.Protocol.Resources; -using Autofac; +using Autofac; using PKISharp.WACS.Clients.Acme; using PKISharp.WACS.Configuration; using PKISharp.WACS.DomainObjects; @@ -12,6 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using acme = ACMESharp.Protocol.Resources; namespace PKISharp.WACS { @@ -27,6 +26,27 @@ namespace PKISharp.WACS private readonly IInputService _input; private readonly ExceptionHandler _exceptionHandler; + /// <summary> + /// Common objects used throughout the renewal process + /// </summary> + private class ExecutionContext + { + public ILifetimeScope Scope { get; private set; } + public Order Order { get; private set; } + public RunLevel RunLevel { get; private set; } + public RenewResult Result { get; private set; } + public Target Target => Order.Target; + public Renewal Renewal => Order.Renewal; + + public ExecutionContext(ILifetimeScope scope, Order order, RunLevel runLevel, RenewResult result) + { + Scope = scope; + Order = order; + RunLevel = runLevel; + Result = result; + } + } + public RenewalExecutor( MainArguments args, IAutofacBuilder scopeBuilder, ILogService log, IInputService input, @@ -40,7 +60,13 @@ namespace PKISharp.WACS _container = container; } - public async Task<RenewResult?> Execute(Renewal renewal, RunLevel runLevel) + /// <summary> + /// Determine if the renewal should be executes + /// </summary> + /// <param name="renewal"></param> + /// <param name="runLevel"></param> + /// <returns></returns> + public async Task<RenewResult> HandleRenewal(Renewal renewal, RunLevel runLevel) { using var ts = _scopeBuilder.Target(_container, renewal, runLevel); using var es = _scopeBuilder.Execution(ts, renewal, runLevel); @@ -68,6 +94,15 @@ namespace PKISharp.WACS throw new Exception($"Validation plugin is unable to validate the target. A wildcard host was introduced into a HTTP validated renewal."); } + // Create one or more orders based on the target + var orderPlugin = es.Resolve<IOrderPlugin>(); + var orders = orderPlugin.Split(renewal, target); + if (orders == null || orders.Count() == 0) + { + throw new Exception("Order plugin failed to create order(s)"); + } + _log.Verbose("Targeted convert into {n} order(s)", orders.Count()); + // Check if renewal is needed if (!runLevel.HasFlag(RunLevel.ForceRenew) && !renewal.Updated) { @@ -75,15 +110,21 @@ namespace PKISharp.WACS if (!renewal.IsDue()) { var cs = es.Resolve<ICertificateService>(); - var cache = cs.CachedInfo(renewal, target); - if (cache != null) + var abort = true; + foreach (var order in orders) { - _log.Information("Renewal for {renewal} is due after {date}", renewal.LastFriendlyName, renewal.GetDueDate()); - return null; + var cache = cs.CachedInfo(order); + if (cache == null && !renewal.New) + { + _log.Information(LogType.All, "Renewal for {renewal} running prematurely due to detected target change", renewal.LastFriendlyName); + abort = false; + break; + } } - else if (!renewal.New) + if (abort) { - _log.Information(LogType.All, "Renewal for {renewal} running prematurely due to detected target change", renewal.LastFriendlyName); + _log.Information("Renewal for {renewal} is due after {date}", renewal.LastFriendlyName, renewal.GetDueDate()); + return new RenewResult() { Abort = true }; } } else if (!renewal.New) @@ -96,103 +137,152 @@ namespace PKISharp.WACS _log.Information(LogType.All, "Force renewing certificate for {renewal}", renewal.LastFriendlyName); } - // Create the order - var orderManager = es.Resolve<OrderManager>(); - var order = await orderManager.GetOrCreate(renewal, target, runLevel); - if (order == null) + // If at this point we haven't retured already with an error/abort + // actually execute the renewal + var result = await ExecuteRenewal(es, orders.ToList(), runLevel); + + // Configure task scheduler + if (result.Success && !result.Abort) { - return OnRenewFail(new Challenge() { Error = "Unable to create order" }); + if ((renewal.New || renewal.Updated) && !_args.NoTaskScheduler) + { + if (runLevel.HasFlag(RunLevel.Test) && !await _input.PromptYesNo($"[--test] Do you want to automatically renew with these settings?", true)) + { + // Early out for test runs + result.Abort = true; + return result; + } + else + { + // Make sure the Task Scheduler is configured + await es.Resolve<TaskSchedulerService>().EnsureTaskScheduler(runLevel, false); + } + } } + return result; + } - // Answer the challenges - var client = es.Resolve<AcmeClient>(); - foreach (var authUrl in order.Payload.Authorizations) + /// <summary> + /// Run the renewal + /// </summary> + /// <param name="execute"></param> + /// <param name="orders"></param> + /// <param name="runLevel"></param> + /// <returns></returns> + private async Task<RenewResult> ExecuteRenewal(ILifetimeScope execute, List<Order> orders, RunLevel runLevel) + { + var result = new RenewResult(); + foreach (var order in orders) { - // Get authorization details - _log.Verbose("Handle authorization {n}/{m}", - order.Payload.Authorizations.ToList().IndexOf(authUrl) + 1, - order.Payload.Authorizations.Length + 1); + _log.Verbose("Handle order {n}/{m}: {friendly}", + orders.IndexOf(order) + 1, + orders.Count, + order.FriendlyNamePart ?? "Main"); - var authorization = await client.GetAuthorizationDetails(authUrl); + // Create the order details + var orderManager = execute.Resolve<OrderManager>(); + order.Details = await orderManager.GetOrCreate(order, runLevel); - // Find a targetPart that matches the challenge - var targetPart = target.Parts. - FirstOrDefault(tp => tp.GetHosts(false). - Any(h => authorization.Identifier.Value == h.Replace("*.", ""))); - if (targetPart == null) - { - return OnRenewFail(new Challenge() - { - Error = "Unable to match challenge to target" - }); - } + // Create the execution context + var context = new ExecutionContext(execute, order, runLevel, result); - // Run the validation plugin - var challenge = await Authorize(es, runLevel, renewal.ValidationPluginOptions, targetPart, authorization); - if (challenge.Status != AcmeClient.AuthorizationValid) + // Authorize the order (validation) + await AuthorizeOrder(context); + if (context.Result.Success) { - return OnRenewFail(challenge); + // Execute final steps (CSR, store, install) + await ExecuteOrder(context); } } - return await OnValidationSuccess(es, renewal, target, order, runLevel); + return result; } /// <summary> - /// Steps to take on authorization failed + /// Answer all the challenges in the order /// </summary> - /// <param name="auth"></param> + /// <param name="execute"></param> + /// <param name="order"></param> + /// <param name="result"></param> + /// <param name="runLevel"></param> /// <returns></returns> - private RenewResult OnRenewFail(Challenge challenge) + private async Task AuthorizeOrder(ExecutionContext context) { - var errors = challenge?.Error; - if (errors != null) + // Sanity check + if (context.Order.Details == null) { - return new RenewResult($"Authorization failed: {errors.ToString()}"); - } - else + context.Result.AddErrorMessage($"Unable to create order"); + return; + } + + // Answer the challenges + var client = context.Scope.Resolve<AcmeClient>(); + var authorizations = context.Order.Details.Payload.Authorizations.ToList(); + foreach (var authorizationUri in authorizations) { - return new RenewResult($"Authorization failed"); + _log.Verbose("Handle authorization {n}/{m}", + authorizations.IndexOf(authorizationUri) + 1, + authorizations.Count); + + // Get authorization challenge details from server + var authorization = await client.GetAuthorizationDetails(authorizationUri); + + // Find a targetPart that matches the challenge + var targetPart = context.Target.Parts. + FirstOrDefault(tp => tp.GetHosts(false). + Any(h => authorization.Identifier.Value == h.Replace("*.", ""))); + if (targetPart == null) + { + context.Result.AddErrorMessage("Unable to match challenge to target"); + return; + } + + // Run the validation plugin + await HandleChallenge(context, targetPart, authorization); } } /// <summary> /// Steps to take on succesful (re)authorization /// </summary> - /// <param name="target"></param> - private async Task<RenewResult?> OnValidationSuccess(ILifetimeScope renewalScope, Renewal renewal, Target target, OrderDetails order, RunLevel runLevel) + /// <param name="partialTarget"></param> + private async Task ExecuteOrder(ExecutionContext context) { - RenewResult? result = null; try { - var certificateService = renewalScope.Resolve<ICertificateService>(); - var csrPlugin = target.CsrBytes == null ? renewalScope.Resolve<ICsrPlugin>() : null; + var certificateService = context.Scope.Resolve<ICertificateService>(); + var csrPlugin = context.Target.CsrBytes == null ? + context.Scope.Resolve<ICsrPlugin>() : + null; if (csrPlugin != null) { var (disabled, disabledReason) = csrPlugin.Disabled; if (disabled) { - return new RenewResult($"CSR plugin is not available. {disabledReason}"); + context.Result.AddErrorMessage($"CSR plugin is not available. {disabledReason}"); + return; } } - var oldCertificate = certificateService.CachedInfo(renewal); - var newCertificate = await certificateService.RequestCertificate(csrPlugin, runLevel, renewal, target, order); + var oldCertificate = certificateService.CachedInfo(context.Order); + var newCertificate = await certificateService.RequestCertificate(csrPlugin, context.RunLevel, context.Order); // Test if a new certificate has been generated if (newCertificate == null) { - return new RenewResult("No certificate generated"); + context.Result.AddErrorMessage("No certificate generated"); + return; } else { - result = new RenewResult(newCertificate); + context.Result.AddThumbprint(newCertificate.Certificate.Thumbprint); } // Early escape for testing validation only - if (renewal.New && - runLevel.HasFlag(RunLevel.Test) && + if (context.Renewal.New && + context.RunLevel.HasFlag(RunLevel.Test) && !await _input.PromptYesNo($"[--test] Do you want to install the certificate?", true)) { - return null; + context.Result.Abort = true; + return; } // Run store plugin(s) @@ -200,11 +290,11 @@ namespace PKISharp.WACS var storePlugins = new List<IStorePlugin>(); try { - var steps = renewal.StorePluginOptions.Count(); + var steps = context.Renewal.StorePluginOptions.Count(); for (var i = 0; i < steps; i++) { - var storeOptions = renewal.StorePluginOptions[i]; - var storePlugin = (IStorePlugin)renewalScope.Resolve(storeOptions.Instance); + var storeOptions = context.Renewal.StorePluginOptions[i]; + var storePlugin = (IStorePlugin)context.Scope.Resolve(storeOptions.Instance); if (!(storePlugin is INull)) { if (steps > 1) @@ -218,7 +308,8 @@ namespace PKISharp.WACS var (disabled, disabledReason) = storePlugin.Disabled; if (disabled) { - return new RenewResult($"Store plugin is not available. {disabledReason}"); + context.Result.AddErrorMessage($"Store plugin is not available. {disabledReason}"); + return; } await storePlugin.Save(newCertificate); storePlugins.Add(storePlugin); @@ -229,19 +320,18 @@ namespace PKISharp.WACS catch (Exception ex) { var reason = _exceptionHandler.HandleException(ex, "Unable to store certificate"); - result.ErrorMessage = $"Store failed: {reason}"; - result.Success = false; - return result; + context.Result.AddErrorMessage($"Store failed: {reason}"); + return; } // Run installation plugin(s) try { - var steps = renewal.InstallationPluginOptions.Count(); + var steps = context.Renewal.InstallationPluginOptions.Count(); for (var i = 0; i < steps; i++) { - var installOptions = renewal.InstallationPluginOptions[i]; - var installPlugin = (IInstallationPlugin)renewalScope.Resolve( + var installOptions = context.Renewal.InstallationPluginOptions[i]; + var installPlugin = (IInstallationPlugin)context.Scope.Resolve( installOptions.Instance, new TypedParameter(installOptions.GetType(), installOptions)); @@ -258,17 +348,18 @@ namespace PKISharp.WACS var (disabled, disabledReason) = installPlugin.Disabled; if (disabled) { - return new RenewResult($"Installation plugin is not available. {disabledReason}"); + context.Result.AddErrorMessage($"Installation plugin is not available. {disabledReason}"); + return; } - await installPlugin.Install(storePlugins, newCertificate, oldCertificate); + await installPlugin.Install(context.Target, storePlugins, newCertificate, oldCertificate); } } } catch (Exception ex) { var reason = _exceptionHandler.HandleException(ex, "Unable to install certificate"); - result.Success = false; - result.ErrorMessage = $"Install failed: {reason}"; + context.Result.AddErrorMessage($"Install failed: {reason}"); + return; } // Delete the old certificate if not forbidden, found and not re-used @@ -285,52 +376,17 @@ namespace PKISharp.WACS catch (Exception ex) { _log.Error(ex, "Unable to delete previous certificate"); - //result.Success = false; // not a show-stopper, consider the renewal a success - result.ErrorMessage = $"Delete failed: {ex.Message}"; + // not a show-stopper, consider the renewal a success + context.Result.AddErrorMessage($"Delete failed: {ex.Message}", false); } } } - - if ((renewal.New || renewal.Updated) && !_args.NoTaskScheduler) - { - if (runLevel.HasFlag(RunLevel.Test) && - !await _input.PromptYesNo($"[--test] Do you want to automatically renew this certificate?", true)) - { - // Early out for test runs - return null; - } - else - { - // Make sure the Task Scheduler is configured - await renewalScope.Resolve<TaskSchedulerService>().EnsureTaskScheduler(runLevel, false); - } - } - - return result; } catch (Exception ex) { - _exceptionHandler.HandleException(ex); - while (ex.InnerException != null) - { - ex = ex.InnerException; - } - - // Result might still contain the Thumbprint of the certificate - // that was requested and (partially? installed, which might help - // with debugging - if (result == null) - { - result = new RenewResult(ex.Message); - } - else - { - result.Success = false; - result.ErrorMessage = ex.Message; - } + var message = _exceptionHandler.HandleException(ex); + context.Result.AddErrorMessage(message); } - - return result; } /// <summary> @@ -338,37 +394,27 @@ namespace PKISharp.WACS /// </summary> /// <param name="target"></param> /// <returns></returns> - private async Task<Challenge> Authorize( - ILifetimeScope execute, RunLevel runLevel, - ValidationPluginOptions options, TargetPart targetPart, - Authorization authorization) + private async Task HandleChallenge(ExecutionContext context, TargetPart targetPart, acme.Authorization authorization) { - var invalid = new Challenge { Status = AcmeClient.AuthorizationInvalid }; - var valid = new Challenge { Status = AcmeClient.AuthorizationValid }; - var client = execute.Resolve<AcmeClient>(); + var valid = false; + var client = context.Scope.Resolve<AcmeClient>(); var identifier = authorization.Identifier.Value; + var options = context.Renewal.ValidationPluginOptions; IValidationPlugin? validationPlugin = null; - using var validation = _scopeBuilder.Validation(execute, options, targetPart, identifier); + using var validation = _scopeBuilder.Validation(context.Scope, options, targetPart, identifier); try { if (authorization.Status == AcmeClient.AuthorizationValid) { - if (!runLevel.HasFlag(RunLevel.Test) && - !runLevel.HasFlag(RunLevel.IgnoreCache)) + _log.Information("Cached authorization result for {identifier}: {Status}", identifier, authorization.Status); + if (!context.RunLevel.HasFlag(RunLevel.Test) && + !context.RunLevel.HasFlag(RunLevel.IgnoreCache)) { - _log.Information("Cached authorization result for {identifier}: {Status}", identifier, authorization.Status); - return valid; - } - - if (runLevel.HasFlag(RunLevel.IgnoreCache)) - { - // Due to the IgnoreCache flag (--force switch) - // we are going to attempt to re-authorize the - // domain even though its already autorized. - // On failure, we can still use the cached result. - // This helps for migration scenarios. - invalid = valid; + return; } + // Used to make --force or --test re-validation errors non-fatal + _log.Information("Handling challenge anyway because --test and/or --force is active"); + valid = true; } _log.Information("Authorize identifier {identifier}", identifier); @@ -377,48 +423,39 @@ namespace PKISharp.WACS var challenge = authorization.Challenges.FirstOrDefault(c => string.Equals(c.Type, options.ChallengeType, StringComparison.CurrentCultureIgnoreCase)); if (challenge == null) { - if (authorization.Status == AcmeClient.AuthorizationValid) + if (valid) { var usedType = authorization.Challenges. - Where(x => x.Status == AcmeClient.AuthorizationValid). + Where(x => x.Status == AcmeClient.ChallengeValid). FirstOrDefault(); _log.Warning("Expected challenge type {type} not available for {identifier}, already validated using {valided}.", options.ChallengeType, authorization.Identifier.Value, usedType?.Type ?? "[unknown]"); - return valid; + return; } else { _log.Error("Expected challenge type {type} not available for {identifier}.", options.ChallengeType, authorization.Identifier.Value); - invalid.Error = "Expected challenge type not available"; - return invalid; + context.Result.AddErrorMessage("Expected challenge type not available", !valid); + return; } } else { _log.Verbose("Initial challenge status: {status}", challenge.Status); - if (challenge.Status == AcmeClient.AuthorizationValid) + if (challenge.Status == AcmeClient.ChallengeValid) { // We actually should not get here because if one of the // challenges is valid, the authorization itself should also // be valid. - if (!runLevel.HasFlag(RunLevel.Test) && - !runLevel.HasFlag(RunLevel.IgnoreCache)) - { - _log.Information("Cached authorization result: {Status}", authorization.Status); - return valid; - } - if (runLevel.HasFlag(RunLevel.IgnoreCache)) + if (!context.RunLevel.HasFlag(RunLevel.Test) && + !context.RunLevel.HasFlag(RunLevel.IgnoreCache)) { - // Due to the IgnoreCache flag (--force switch) - // we are going to attempt to re-authorize the - // domain even though its already autorized. - // On failure, we can still use the cached result. - // This helps for migration scenarios. - invalid = valid; + _log.Information("Cached challenge result: {Status}", authorization.Status); + return; } } } @@ -434,16 +471,16 @@ namespace PKISharp.WACS } if (validationPlugin == null) { - _log.Error("Validation plugin not found or not created."); - invalid.Error = "Validation plugin not found or not created."; - return invalid; + _log.Error("Validation plugin not found or not created"); + context.Result.AddErrorMessage("Validation plugin not found or not created", !valid); + return; } var (disabled, disabledReason) = validationPlugin.Disabled; if (disabled) { _log.Error($"Validation plugin is not available. {disabledReason}"); - invalid.Error = "Validation plugin is not available."; - return invalid; + context.Result.AddErrorMessage("Validation plugin is not available", !valid); + return; } _log.Information("Authorizing {dnsIdentifier} using {challengeType} validation ({name})", identifier, @@ -457,34 +494,33 @@ namespace PKISharp.WACS catch (Exception ex) { _log.Error(ex, "Error preparing for challenge answer"); - invalid.Error = "Error preparing for challenge answer"; - return invalid; + context.Result.AddErrorMessage("Error preparing for challenge answer", !valid); + return; } _log.Debug("Submitting challenge answer"); challenge = await client.AnswerChallenge(challenge); - if (challenge.Status != AcmeClient.AuthorizationValid) + if (challenge.Status != AcmeClient.ChallengeValid) { if (challenge.Error != null) { _log.Error(challenge.Error.ToString()); } _log.Error("Authorization result: {Status}", challenge.Status); - invalid.Error = challenge.Error; - return invalid; + context.Result.AddErrorMessage(challenge.Error?.ToString() ?? "Unspecified error", !valid); + return; } else { _log.Information("Authorization result: {Status}", challenge.Status); - return valid; + return; } } catch (Exception ex) { _log.Error("Error authorizing {renewal}", targetPart); - _exceptionHandler.HandleException(ex); - invalid.Error = ex.Message; - return invalid; + var message = _exceptionHandler.HandleException(ex); + context.Result.AddErrorMessage(message, !valid); } finally { diff --git a/src/main.lib/RenewalManager.cs b/src/main.lib/RenewalManager.cs index 0410339..424e1fe 100644 --- a/src/main.lib/RenewalManager.cs +++ b/src/main.lib/RenewalManager.cs @@ -198,20 +198,7 @@ namespace PKISharp.WACS var confirm = await _input.PromptYesNo($"Are you sure you want to revoke the most recently issued certificate for {selectedRenewals.Count()} currently selected {renewalSelectedLabel}? This should only be done in case of a (suspected) security breach. Cancel the {renewalSelectedLabel} if you simply don't need the certificates anymore.", false); if (confirm) { - foreach (var renewal in selectedRenewals) - { - using var scope = _scopeBuilder.Execution(_container, renewal, RunLevel.Interactive); - var cs = scope.Resolve<ICertificateService>(); - try - { - await cs.RevokeCertificate(renewal); - renewal.History.Add(new RenewResult("Certificate revoked")); - } - catch (Exception ex) - { - _exceptionHandler.HandleException(ex); - } - }; + await RevokeCertificates(selectedRenewals); } }, $"Revoke certificate for {selectionLabel}", "V", @@ -502,8 +489,8 @@ namespace PKISharp.WACS var notification = _container.Resolve<NotificationService>(); try { - var result = await _renewalExecutor.Execute(renewal, runLevel); - if (result != null) + var result = await _renewalExecutor.HandleRenewal(renewal, runLevel); + if (!result.Abort) { _renewalStore.Save(renewal, result); if (result.Success) @@ -512,14 +499,14 @@ namespace PKISharp.WACS } else { - notification.NotifyFailure(runLevel, renewal, result.ErrorMessage); + notification.NotifyFailure(runLevel, renewal, result.ErrorMessages); } } } catch (Exception ex) { _exceptionHandler.HandleException(ex); - notification.NotifyFailure(runLevel, renewal, ex.Message); + notification.NotifyFailure(runLevel, renewal, new List<string> { ex.Message }); } } @@ -553,6 +540,10 @@ namespace PKISharp.WACS _input.Show("Renewed", $"{renewal.History.Where(x => x.Success).Count()} times"); renewal.TargetPluginOptions.Show(_input); renewal.ValidationPluginOptions.Show(_input); + if (renewal.OrderPluginOptions != null) + { + renewal.OrderPluginOptions.Show(_input); + } if (renewal.CsrPluginOptions != null) { renewal.CsrPluginOptions.Show(_input); @@ -613,6 +604,16 @@ namespace PKISharp.WACS { _log.Warning($"Certificates should only be revoked in case of a (suspected) security breach. Cancel the renewal if you simply don't need the certificate anymore."); var renewals = await FilterRenewalsByCommandLine("revoke"); + await RevokeCertificates(renewals); + } + + /// <summary> + /// Shared code for command line and renewal manager + /// </summary> + /// <param name="renewals"></param> + /// <returns></returns> + internal async Task RevokeCertificates(IEnumerable<Renewal> renewals) + { foreach (var renewal in renewals) { using var scope = _scopeBuilder.Execution(_container, renewal, RunLevel.Unattended); diff --git a/src/main.lib/Services/ArgumentsParser.cs b/src/main.lib/Services/ArgumentsParser.cs index 0c974f1..20529f0 100644 --- a/src/main.lib/Services/ArgumentsParser.cs +++ b/src/main.lib/Services/ArgumentsParser.cs @@ -9,7 +9,7 @@ namespace PKISharp.WACS.Configuration { private readonly ILogService _log; private readonly string[] _args; - private readonly List<IArgumentsProvider> _providers; + private readonly IEnumerable<IArgumentsProvider> _providers; public T GetArguments<T>() where T : class, new() { @@ -27,7 +27,7 @@ namespace PKISharp.WACS.Configuration { _log = log; _args = args; - _providers = plugins.OptionProviders(); + _providers = plugins.ArgumentsProviders(); } internal bool Validate() diff --git a/src/main.lib/Services/AutofacBuilder.cs b/src/main.lib/Services/AutofacBuilder.cs index b94f599..5a5e4f9 100644 --- a/src/main.lib/Services/AutofacBuilder.cs +++ b/src/main.lib/Services/AutofacBuilder.cs @@ -125,6 +125,7 @@ namespace PKISharp.WACS.Services builder.RegisterType(renewal.TargetPluginOptions.Instance).As<ITargetPlugin>().SingleInstance(); builder.Register(c => c.Resolve<ITargetPlugin>().Generate().Result).As<Target>().SingleInstance(); builder.Register(c => resolver.GetValidationPlugin(main, c.Resolve<Target>()).Result).As<IValidationPluginOptionsFactory>().SingleInstance(); + builder.Register(c => resolver.GetOrderPlugin(main, c.Resolve<Target>()).Result).As<IOrderPluginOptionsFactory>().SingleInstance(); }); } @@ -152,6 +153,10 @@ namespace PKISharp.WACS.Services { builder.RegisterInstance(renewal.CsrPluginOptions).As(renewal.CsrPluginOptions.GetType()); } + if (renewal.OrderPluginOptions != null) + { + builder.RegisterInstance(renewal.OrderPluginOptions).As(renewal.OrderPluginOptions.GetType()); + } builder.RegisterInstance(renewal.ValidationPluginOptions).As(renewal.ValidationPluginOptions.GetType()); builder.RegisterInstance(renewal.TargetPluginOptions).As(renewal.TargetPluginOptions.GetType()); @@ -159,7 +164,7 @@ namespace PKISharp.WACS.Services builder.Register(x => { var plugin = x.Resolve<IPluginService>(); - var match = plugin.ValidationPluginFactories(target).FirstOrDefault(vp => vp.OptionsType.PluginId() == renewal.ValidationPluginOptions.Plugin); + var match = plugin.GetFactories<IValidationPluginOptionsFactory>(target).FirstOrDefault(vp => vp.OptionsType.PluginId() == renewal.ValidationPluginOptions.Plugin); return match; }).As<IValidationPluginOptionsFactory>().SingleInstance(); @@ -167,6 +172,14 @@ namespace PKISharp.WACS.Services { builder.RegisterType(renewal.CsrPluginOptions.Instance).As<ICsrPlugin>().SingleInstance(); } + if (renewal.OrderPluginOptions != null) + { + builder.RegisterType(renewal.OrderPluginOptions.Instance).As<IOrderPlugin>().SingleInstance(); + } + else + { + builder.RegisterType<Plugins.OrderPlugins.Single>().As<IOrderPlugin>().SingleInstance(); + } builder.RegisterType(renewal.ValidationPluginOptions.Instance).As<IValidationPlugin>().SingleInstance(); builder.RegisterType(renewal.TargetPluginOptions.Instance).As<ITargetPlugin>().SingleInstance(); foreach (var i in renewal.InstallationPluginOptions) diff --git a/src/main.lib/Services/CertificateService.cs b/src/main.lib/Services/CertificateService.cs index a0fabbe..eca6861 100644 --- a/src/main.lib/Services/CertificateService.cs +++ b/src/main.lib/Services/CertificateService.cs @@ -11,7 +11,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; @@ -110,7 +109,7 @@ namespace PKISharp.WACS.Services { foreach (var f in _cache.EnumerateFiles($"*.keys")) { - var x = new ProtectedString(File.ReadAllText(f.FullName)); + var x = new ProtectedString(File.ReadAllText(f.FullName), _log); _log.Information("Rewriting {x}", f.Name); File.WriteAllText(f.FullName, x.DiskValue(_settings.Security.EncryptConfig)); } @@ -130,58 +129,52 @@ namespace PKISharp.WACS.Services /// </summary> /// <param name="renewal"></param> /// <returns></returns> - public CertificateInfo? CachedInfo(Renewal renewal, Target? target = null) + public CertificateInfo? CachedInfo(Order order) { - var nameAll = GetPath(renewal, ""); - var directory = new DirectoryInfo(Path.GetDirectoryName(nameAll)); - var allPattern = Path.GetFileName(nameAll); - var allFiles = directory.EnumerateFiles(allPattern + "*"); - if (!allFiles.Any()) + var cachedInfos = CachedInfos(order.Renewal); + if (!cachedInfos.Any()) { return null; } - FileInfo? fileCache = null; - if (target != null) + var keyName = GetPath(order.Renewal, $"-{CacheKey(order)}{PfxPostFix}"); + var fileCache = cachedInfos.Where(x => x.CacheFile?.FullName == keyName).FirstOrDefault(); + if (fileCache == null) { - var key = CacheKey(renewal, target); - var keyName = Path.GetFileName(GetPath(renewal, $"-{key}{PfxPostFix}")); - var keyFile = allFiles.Where(x => x.Name == keyName).FirstOrDefault(); - if (keyFile != null) - { - fileCache = keyFile; - } - else + var legacyFile = GetPath(order.Renewal, PfxPostFixLegacy); + var candidate = cachedInfos.Where(x => x.CacheFile?.FullName == legacyFile).FirstOrDefault(); + if (candidate != null) { - var legacyFile = new FileInfo(GetPath(renewal, PfxPostFixLegacy)); - if (legacyFile.Exists) + if (Match(candidate, order.Target)) { - var legacyInfo = FromCache(legacyFile, renewal.PfxPassword?.Value); - if (Match(legacyInfo, target)) - { - fileCache = legacyFile; - } + fileCache = candidate; } } } - else - { - fileCache = allFiles.OrderByDescending(x => x.LastWriteTime).FirstOrDefault(); - } + return fileCache; + } - if (fileCache != null) + public IEnumerable<CertificateInfo> CachedInfos(Renewal renewal) + { + var ret = new List<CertificateInfo>(); + var nameAll = GetPath(renewal, "*.pfx"); + var directory = new DirectoryInfo(Path.GetDirectoryName(nameAll)); + var allPattern = Path.GetFileName(nameAll); + var allFiles = directory.EnumerateFiles(allPattern + "*"); + var fileCache = allFiles.OrderByDescending(x => x.LastWriteTime); + foreach (var file in fileCache) { try { - return FromCache(fileCache, renewal.PfxPassword?.Value); + ret.Add(FromCache(file, renewal.PfxPassword?.Value)); } catch { // File corrupt or invalid password? - _log.Warning("Unable to read from certificate cache"); + _log.Warning("Unable to read {i} from certificate cache", file.Name); } } - return null; + return ret; } /// <summary> @@ -209,19 +202,20 @@ namespace PKISharp.WACS.Services /// <param name="renewal"></param> /// <param name="target"></param> /// <returns></returns> - public string CacheKey(Renewal renewal, Target target) + public string CacheKey(Order order) { // Check if we can reuse a cached certificate and/or order // based on currently active set of parameters and shape of // the target. var cacheKeyBuilder = new StringBuilder(); - cacheKeyBuilder.Append(target.CommonName); - cacheKeyBuilder.Append(string.Join(',', target.GetHosts(true).OrderBy(x => x).Select(x => x.ToLower()))); - _ = target.CsrBytes != null ? - cacheKeyBuilder.Append(Convert.ToBase64String(target.CsrBytes)) : + cacheKeyBuilder.Append(order.CacheKeyPart); + cacheKeyBuilder.Append(order.Target.CommonName); + cacheKeyBuilder.Append(string.Join(',', order.Target.GetHosts(true).OrderBy(x => x).Select(x => x.ToLower()))); + _ = order.Target.CsrBytes != null ? + cacheKeyBuilder.Append(Convert.ToBase64String(order.Target.CsrBytes)) : cacheKeyBuilder.Append("-"); - _ = renewal.CsrPluginOptions != null ? - cacheKeyBuilder.Append(JsonConvert.SerializeObject(renewal.CsrPluginOptions)) : + _ = order.Renewal.CsrPluginOptions != null ? + cacheKeyBuilder.Append(JsonConvert.SerializeObject(order.Renewal.CsrPluginOptions)) : cacheKeyBuilder.Append("-"); return cacheKeyBuilder.ToString().SHA1(); } @@ -235,20 +229,20 @@ namespace PKISharp.WACS.Services /// <param name="target"></param> /// <param name="order"></param> /// <returns></returns> - public async Task<CertificateInfo> RequestCertificate( - ICsrPlugin? csrPlugin, - RunLevel runLevel, - Renewal renewal, - Target target, - OrderDetails order) + public async Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, RunLevel runLevel, Order order) { + if (order.Details == null) + { + throw new InvalidOperationException(); + } + // What are we going to get? - var cacheKey = CacheKey(renewal, target); - var pfxFileInfo = new FileInfo(GetPath(renewal, $"-{cacheKey}{PfxPostFix}")); + var cacheKey = CacheKey(order); + var pfxFileInfo = new FileInfo(GetPath(order.Renewal, $"-{cacheKey}{PfxPostFix}")); // Determine/check the common name - var identifiers = target.GetHosts(false); - var commonNameUni = target.CommonName; + var identifiers = order.Target.GetHosts(false); + var commonNameUni = order.Target.CommonName; var commonNameAscii = string.Empty; if (!string.IsNullOrWhiteSpace(commonNameUni)) { @@ -262,23 +256,30 @@ namespace PKISharp.WACS.Services } } - // Determine the friendly name - var friendlyNameBase = renewal.FriendlyName; + // Determine the friendly name base (for the renewal) + var friendlyNameBase = order.Renewal.FriendlyName; if (string.IsNullOrEmpty(friendlyNameBase)) { - friendlyNameBase = target.FriendlyName; + friendlyNameBase = order.Target.FriendlyName; } if (string.IsNullOrEmpty(friendlyNameBase)) { friendlyNameBase = commonNameUni; } - var friendyName = $"{friendlyNameBase} @ {_inputService.FormatDate(DateTime.Now)}"; + + // Determine the friendly name for this specific certificate + var friendlyNameIntermediate = friendlyNameBase; + if (!string.IsNullOrEmpty(order.FriendlyNamePart)) + { + friendlyNameIntermediate += $" [{order.FriendlyNamePart}]"; + } + var friendlyName = $"{friendlyNameIntermediate} @ {_inputService.FormatDate(DateTime.Now)}"; // Try using cached certificate first to avoid rate limiting during // (initial?) deployment troubleshooting. Real certificate requests // will only be done once per day maximum unless the --force parameter // is used. - var cache = CachedInfo(renewal, target); + var cache = CachedInfo(order); if (cache != null && cache.CacheFile != null) { if (cache.CacheFile.LastWriteTime > DateTime.Now.AddDays(_settings.Cache.ReuseDays * -1)) @@ -292,7 +293,7 @@ namespace PKISharp.WACS.Services { _log.Warning("Using cached certificate for {friendlyName}. To force a new request of the " + "certificate within {days} days, run with the --{switch} switch.", - friendlyNameBase, + friendlyNameIntermediate, _settings.Cache.ReuseDays, nameof(MainArguments.Force).ToLower()); return cache; @@ -300,39 +301,39 @@ namespace PKISharp.WACS.Services } } - if (order.Payload.Status != AcmeClient.OrderValid) + if (order.Details.Payload.Status != AcmeClient.OrderValid) { // Clear cache and write new cert - ClearCache(renewal, postfix: CsrPostFix); + ClearCache(order.Renewal, postfix: CsrPostFix); - if (target.CsrBytes == null) + if (order.Target.CsrBytes == null) { if (csrPlugin == null) { throw new InvalidOperationException("Missing csrPlugin"); } - var keyFile = GetPath(renewal, ".keys"); + var keyFile = GetPath(order.Renewal, ".keys"); var csr = await csrPlugin.GenerateCsr(keyFile, commonNameAscii, identifiers); var keySet = await csrPlugin.GetKeys(); - target.CsrBytes = csr.GetDerEncoded(); - target.PrivateKey = keySet.Private; - var csrPath = GetPath(renewal, CsrPostFix); - File.WriteAllText(csrPath, _pemService.GetPem("CERTIFICATE REQUEST", target.CsrBytes)); + order.Target.CsrBytes = csr.GetDerEncoded(); + order.Target.PrivateKey = keySet.Private; + var csrPath = GetPath(order.Renewal, CsrPostFix); + File.WriteAllText(csrPath, _pemService.GetPem("CERTIFICATE REQUEST", order.Target.CsrBytes)); _log.Debug("CSR stored at {path} in certificate cache folder {folder}", Path.GetFileName(csrPath), Path.GetDirectoryName(csrPath)); } _log.Verbose("Submitting CSR"); - order = await _client.SubmitCsr(order, target.CsrBytes); - if (order.Payload.Status != AcmeClient.OrderValid) + order.Details = await _client.SubmitCsr(order.Details, order.Target.CsrBytes); + if (order.Details.Payload.Status != AcmeClient.OrderValid) { - _log.Error("Unexpected order status {status}", order.Payload.Status); + _log.Error("Unexpected order status {status}", order.Details.Payload.Status); throw new Exception($"Unable to complete order"); } } - _log.Information("Requesting certificate {friendlyName}", friendlyNameBase); - var rawCertificate = await _client.GetCertificate(order); + _log.Information("Requesting certificate {friendlyName}", friendlyNameIntermediate); + var rawCertificate = await _client.GetCertificate(order.Details); if (rawCertificate == null) { throw new Exception($"Unable to get certificate"); @@ -364,16 +365,16 @@ namespace PKISharp.WACS.Services { var bcCertificateEntry = new bc.Pkcs.X509CertificateEntry(bcCertificate); var bcCertificateAlias = startIndex == 0 ? - friendyName : + friendlyName : bcCertificate.SubjectDN.ToString(); pfx.SetCertificateEntry(bcCertificateAlias, bcCertificateEntry); // Assume that the first certificate in the reponse is the main one // so we associate the private key with that one. Other certificates // are intermediates - if (startIndex == 0 && target.PrivateKey != null) + if (startIndex == 0 && order.Target.PrivateKey != null) { - var bcPrivateKeyEntry = new bc.Pkcs.AsymmetricKeyEntry(target.PrivateKey); + var bcPrivateKeyEntry = new bc.Pkcs.AsymmetricKeyEntry(order.Target.PrivateKey); pfx.SetKeyEntry(bcCertificateAlias, bcPrivateKeyEntry, new[] { bcCertificateEntry }); } } @@ -398,9 +399,9 @@ namespace PKISharp.WACS.Services X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); - ClearCache(renewal, postfix: $"*{PfxPostFix}"); - ClearCache(renewal, postfix: $"*{PfxPostFixLegacy}"); - File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, renewal.PfxPassword?.Value)); + ClearCache(order.Renewal, postfix: $"*{PfxPostFix}"); + ClearCache(order.Renewal, postfix: $"*{PfxPostFixLegacy}"); + File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value)); _log.Debug("Certificate written to cache file {path} in certificate cache folder {folder}. It will be " + "reused when renewing within {x} day(s) as long as the Target and Csr parameters remain the same and " + "the --force switch is not used.", @@ -422,9 +423,9 @@ namespace PKISharp.WACS.Services var newVersion = await csrPlugin.PostProcess(cert); if (newVersion != cert) { - newVersion.FriendlyName = friendyName; + newVersion.FriendlyName = friendlyName; tempPfx[certIndex] = newVersion; - File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, renewal.PfxPassword?.Value)); + File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value)); newVersion.Dispose(); } } @@ -440,10 +441,10 @@ namespace PKISharp.WACS.Services // Update LastFriendlyName so that the user sees // the most recently issued friendlyName in // the WACS GUI - renewal.LastFriendlyName = friendlyNameBase; + order.Renewal.LastFriendlyName = friendlyNameBase; // Recreate X509Certificate2 with correct flags for Store/Install - return FromCache(pfxFileInfo, renewal.PfxPassword?.Value); + return FromCache(pfxFileInfo, order.Renewal.PfxPassword?.Value); } private CertificateInfo FromCache(FileInfo pfxFileInfo, string? password) @@ -501,14 +502,21 @@ namespace PKISharp.WACS.Services public async Task RevokeCertificate(Renewal renewal) { // Delete cached files - var info = CachedInfo(renewal); - if (info != null) + var infos = CachedInfos(renewal); + foreach (var info in infos) { - var certificateDer = info.Certificate.Export(X509ContentType.Cert); - await _client.RevokeCertificate(certificateDer); + try + { + var certificateDer = info.Certificate.Export(X509ContentType.Cert); + await _client.RevokeCertificate(certificateDer); + info.CacheFile?.Delete(); + _log.Warning($"Revoked certificate {info.Certificate.FriendlyName}"); + } + catch (Exception ex) + { + _log.Error(ex, $"Error revoking certificate {info.Certificate.FriendlyName}, you may retry"); + } } - ClearCache(renewal); - _log.Warning("Certificate for {target} revoked, you should renew immediately", renewal); } /// <summary> @@ -517,6 +525,5 @@ namespace PKISharp.WACS.Services /// <param name="friendlyName"></param> /// <returns></returns> public static Func<X509Certificate2, bool> ThumbprintFilter(string thumbprint) => new Func<X509Certificate2, bool>(x => string.Equals(x.Thumbprint, thumbprint)); - } } diff --git a/src/main.lib/Services/InputService.cs b/src/main.lib/Services/InputService.cs index 06416d3..e4f51a1 100644 --- a/src/main.lib/Services/InputService.cs +++ b/src/main.lib/Services/InputService.cs @@ -373,6 +373,13 @@ namespace PKISharp.WACS.Services Where(t => string.Equals(t.Command, choice, StringComparison.InvariantCultureIgnoreCase)). FirstOrDefault(); + if (selected == null) + { + selected = choices. + Where(t => string.Equals(t.Description, choice, StringComparison.InvariantCultureIgnoreCase)). + FirstOrDefault(); + } + if (selected != null && selected.Disabled) { var disabledReason = selected.DisabledReason ?? "Run as Administator to enable all features."; diff --git a/src/main.lib/Services/Interfaces/ICertificateService.cs b/src/main.lib/Services/Interfaces/ICertificateService.cs index 27e8cb8..1d7f4f3 100644 --- a/src/main.lib/Services/Interfaces/ICertificateService.cs +++ b/src/main.lib/Services/Interfaces/ICertificateService.cs @@ -1,15 +1,17 @@ using ACMESharp.Protocol; using PKISharp.WACS.DomainObjects; using PKISharp.WACS.Plugins.Interfaces; +using System.Collections.Generic; using System.Threading.Tasks; namespace PKISharp.WACS.Services { internal interface ICertificateService { - string CacheKey(Renewal renewal, Target target); - CertificateInfo? CachedInfo(Renewal renewal, Target? target = null); - Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, RunLevel runLevel, Renewal renewal, Target target, OrderDetails order); + string CacheKey(Order order); + CertificateInfo? CachedInfo(Order order); + IEnumerable<CertificateInfo> CachedInfos(Renewal renewal); + Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, RunLevel runLevel, Order order); Task RevokeCertificate(Renewal renewal); void Encrypt(); void Delete(Renewal renewal); diff --git a/src/main.lib/Services/Interfaces/IPluginService.cs b/src/main.lib/Services/Interfaces/IPluginService.cs index 790c644..4ef77b4 100644 --- a/src/main.lib/Services/Interfaces/IPluginService.cs +++ b/src/main.lib/Services/Interfaces/IPluginService.cs @@ -8,17 +8,9 @@ namespace PKISharp.WACS.Services { public interface IPluginService { - ICsrPluginOptionsFactory CsrPluginFactory(ILifetimeScope scope, string name); - List<ICsrPluginOptionsFactory> CsrPluginOptionsFactories(ILifetimeScope scope); - List<IInstallationPluginOptionsFactory> InstallationPluginFactories(ILifetimeScope scope); - IInstallationPluginOptionsFactory InstallationPluginFactory(ILifetimeScope scope, string name); - List<IArgumentsProvider> OptionProviders(); - List<Type> PluginOptionTypes<T>() where T : PluginOptions; - List<IStorePluginOptionsFactory> StorePluginFactories(ILifetimeScope scope); - IStorePluginOptionsFactory StorePluginFactory(ILifetimeScope scope, string name); - List<ITargetPluginOptionsFactory> TargetPluginFactories(ILifetimeScope scope); - ITargetPluginOptionsFactory TargetPluginFactory(ILifetimeScope scope, string name); - List<IValidationPluginOptionsFactory> ValidationPluginFactories(ILifetimeScope scope); - IValidationPluginOptionsFactory ValidationPluginFactory(ILifetimeScope scope, string type, string name); + IEnumerable<T> GetFactories<T>(ILifetimeScope scope) where T: IPluginOptionsFactory; + T GetFactory<T>(ILifetimeScope scope, string name, string? parameter = null) where T : IPluginOptionsFactory; + IEnumerable<IArgumentsProvider> ArgumentsProviders(); + IEnumerable<Type> PluginOptionTypes<T>() where T : PluginOptions; } } diff --git a/src/main.lib/Services/Interfaces/ISettingsService.cs b/src/main.lib/Services/Interfaces/ISettingsService.cs index b6504ef..215c732 100644 --- a/src/main.lib/Services/Interfaces/ISettingsService.cs +++ b/src/main.lib/Services/Interfaces/ISettingsService.cs @@ -16,7 +16,11 @@ namespace PKISharp.WACS.Services SecuritySettings Security { get; } ScriptSettings Script { get; } ClientSettings Client { get; } + TargetSettings Target { get; } ValidationSettings Validation { get; } + OrderSettings Order { get; } + CsrSettings Csr { get; } StoreSettings Store { get; } + InstallationSettings Installation { get; } } } diff --git a/src/main.lib/Services/Legacy/LegacySettingsService.cs b/src/main.lib/Services/Legacy/LegacySettingsService.cs index 71be4c3..45d711b 100644 --- a/src/main.lib/Services/Legacy/LegacySettingsService.cs +++ b/src/main.lib/Services/Legacy/LegacySettingsService.cs @@ -22,8 +22,12 @@ namespace PKISharp.WACS.Host.Services.Legacy public NotificationSettings Notification { get; private set; } = new NotificationSettings(); public SecuritySettings Security { get; private set; } = new SecuritySettings(); public ScriptSettings Script { get; private set; } = new ScriptSettings(); + public TargetSettings Target { get; private set; } = new TargetSettings(); public ValidationSettings Validation { get; private set; } = new ValidationSettings(); + public OrderSettings Order { get; private set; } = new OrderSettings(); + public CsrSettings Csr { get; private set; } = new CsrSettings(); public StoreSettings Store { get; private set; } = new StoreSettings(); + public InstallationSettings Installation { get; private set; } = new InstallationSettings(); public string ExePath { get; private set; } public List<string> ClientNames { get; private set; } diff --git a/src/main.lib/Services/NotificationService.cs b/src/main.lib/Services/NotificationService.cs index 282e8aa..62e344e 100644 --- a/src/main.lib/Services/NotificationService.cs +++ b/src/main.lib/Services/NotificationService.cs @@ -2,6 +2,7 @@ using PKISharp.WACS.Clients; using PKISharp.WACS.DomainObjects; using System; +using System.Collections.Generic; using System.Linq; using System.Net.Mail; @@ -49,14 +50,20 @@ namespace PKISharp.WACS.Services /// </summary> /// <param name="runLevel"></param> /// <param name="renewal"></param> - internal void NotifyFailure(RunLevel runLevel, Renewal renewal, string? errorMessage) + internal void NotifyFailure(RunLevel runLevel, Renewal renewal, List<string> errorMessage) { // Do not send emails when running interactively _log.Error("Renewal for {friendlyName} failed, will retry on next run", renewal.LastFriendlyName); + if (errorMessage.Count == 0) + { + errorMessage.Add("No specific error reason provided."); + } if (runLevel.HasFlag(RunLevel.Unattended)) { _email.Send("Error processing certificate renewal", - $"<p>Renewal for <b>{renewal.LastFriendlyName}</b> failed with error <b>{errorMessage ?? "(null)"}</b>, will retry on next run.</p> {NotificationInformation(renewal)}", + @$"<p>Renewal for <b>{renewal.LastFriendlyName}</b> failed with error(s) + <ul><li>{string.Join("</li><li>", errorMessage)}</li></ul> will retry + on next run.</p> {NotificationInformation(renewal)}", MessagePriority.Urgent); } } @@ -70,6 +77,10 @@ namespace PKISharp.WACS.Services extraMessage += "<p><table><tr><td>Plugins</td><td></td></tr>"; extraMessage += $"<tr><td>Target: </td><td> {renewal.TargetPluginOptions.Name}</td></tr>"; extraMessage += $"<tr><td>Validation: </td><td> {renewal.ValidationPluginOptions.Name}</td></tr>"; + if (renewal.OrderPluginOptions != null) + { + extraMessage += $"<tr><td>Order: </td><td> {renewal.OrderPluginOptions.Name}</td></tr>"; + } if (renewal.CsrPluginOptions != null) { extraMessage += $"<tr><td>CSR: </td><td> {renewal.CsrPluginOptions.Name}</td></tr>"; @@ -90,14 +101,14 @@ namespace PKISharp.WACS.Services { try { - var cache = _certificateService.CachedInfo(renewal); - if (cache == null) + var infos = _certificateService.CachedInfos(renewal); + if (infos == null || infos.Count() == 0) { return "Unknown"; } else { - return string.Join(", ", cache.SanNames); + return string.Join(", ", infos.SelectMany(i => i.SanNames).Distinct()); } } catch diff --git a/src/main.lib/Services/PluginService.cs b/src/main.lib/Services/PluginService.cs index fff7fec..404e65d 100644 --- a/src/main.lib/Services/PluginService.cs +++ b/src/main.lib/Services/PluginService.cs @@ -14,103 +14,53 @@ namespace PKISharp.WACS.Services public class PluginService : IPluginService { private readonly List<Type> _allTypes; - - private readonly List<Type> _optionProviders; - - private readonly List<Type> _targetOptionFactories; - private readonly List<Type> _validationOptionFactories; - private readonly List<Type> _csrOptionFactories; - private readonly List<Type> _storeOptionFactories; - private readonly List<Type> _installationOptionFactories; - - private readonly List<Type> _target; - private readonly List<Type> _validation; - private readonly List<Type> _csr; - private readonly List<Type> _store; - private readonly List<Type> _installation; + private readonly List<Type> _argumentProviders; + private readonly List<Type> _optionFactories; + private readonly List<Type> _plugins; internal readonly ILogService _log; - public List<IArgumentsProvider> OptionProviders() + public IEnumerable<IArgumentsProvider> ArgumentsProviders() { - return _optionProviders.Select(x => + return _argumentProviders.Select(x => { var c = x.GetConstructor(new Type[] { }); return (IArgumentsProvider)c.Invoke(new object[] { }); }).ToList(); } - public List<ITargetPluginOptionsFactory> TargetPluginFactories(ILifetimeScope scope) => GetFactories<ITargetPluginOptionsFactory>(_targetOptionFactories, scope); - - public List<IValidationPluginOptionsFactory> ValidationPluginFactories(ILifetimeScope scope) => GetFactories<IValidationPluginOptionsFactory>(_validationOptionFactories, scope); - - public List<ICsrPluginOptionsFactory> CsrPluginOptionsFactories(ILifetimeScope scope) => GetFactories<ICsrPluginOptionsFactory>(_csrOptionFactories, scope); - - public List<IStorePluginOptionsFactory> StorePluginFactories(ILifetimeScope scope) => GetFactories<IStorePluginOptionsFactory>(_storeOptionFactories, scope); - - public List<IInstallationPluginOptionsFactory> InstallationPluginFactories(ILifetimeScope scope) => GetFactories<IInstallationPluginOptionsFactory>(_installationOptionFactories, scope); - - public ITargetPluginOptionsFactory TargetPluginFactory(ILifetimeScope scope, string name) => GetByName<ITargetPluginOptionsFactory>(_targetOptionFactories, name, scope); - - public IValidationPluginOptionsFactory ValidationPluginFactory(ILifetimeScope scope, string type, string name) - { - return _validationOptionFactories. - Select(scope.Resolve). - OfType<IValidationPluginOptionsFactory>(). - FirstOrDefault(x => x.Match(name) && string.Equals(type, x.ChallengeType, StringComparison.InvariantCultureIgnoreCase)); - } - - public ICsrPluginOptionsFactory CsrPluginFactory(ILifetimeScope scope, string name) => GetByName<ICsrPluginOptionsFactory>(_csrOptionFactories, name, scope); - - public IStorePluginOptionsFactory StorePluginFactory(ILifetimeScope scope, string name) => GetByName<IStorePluginOptionsFactory>(_storeOptionFactories, name, scope); - - public IInstallationPluginOptionsFactory InstallationPluginFactory(ILifetimeScope scope, string name) => GetByName<IInstallationPluginOptionsFactory>(_installationOptionFactories, name, scope); - - public List<Type> PluginOptionTypes<T>() where T : PluginOptions => GetResolvable<T>(); + public IEnumerable<Type> PluginOptionTypes<T>() where T : PluginOptions => GetResolvable<T>(); internal void Configure(ContainerBuilder builder) { - _targetOptionFactories.ForEach(t => builder.RegisterType(t).SingleInstance()); - _validationOptionFactories.ForEach(t => builder.RegisterType(t).SingleInstance()); - _csrOptionFactories.ForEach(t => builder.RegisterType(t).SingleInstance()); - _storeOptionFactories.ForEach(t => builder.RegisterType(t).SingleInstance()); - _installationOptionFactories.ForEach(t => builder.RegisterType(t).SingleInstance()); - - _target.ForEach(ip => builder.RegisterType(ip)); - _validation.ForEach(ip => builder.RegisterType(ip)); - _csr.ForEach(ip => builder.RegisterType(ip)); - _store.ForEach(ip => builder.RegisterType(ip)); - _installation.ForEach(ip => builder.RegisterType(ip)); + _optionFactories.ForEach(t => builder.RegisterType(t).SingleInstance()); + _plugins.ForEach(ip => builder.RegisterType(ip)); } - private List<T> GetFactories<T>(List<Type> source, ILifetimeScope scope) where T : IPluginOptionsFactory => source.Select(scope.Resolve).OfType<T>().OrderBy(x => x.Order).ToList(); + public IEnumerable<T> GetFactories<T>(ILifetimeScope scope) where T : IPluginOptionsFactory => _optionFactories.Select(scope.Resolve).OfType<T>().OrderBy(x => x.Order).ToList(); - private T GetByName<T>(IEnumerable<Type> list, string name, ILifetimeScope scope) where T : IPluginOptionsFactory => list.Select(scope.Resolve).OfType<T>().FirstOrDefault(x => x.Match(name)); + private IEnumerable<T> GetByName<T>(string name, ILifetimeScope scope) where T : IPluginOptionsFactory => GetFactories<T>(scope).Where(x => x.Match(name)); public PluginService(ILogService logger) { _log = logger; _allTypes = GetTypes(); - _optionProviders = GetResolvable<IArgumentsProvider>(); - - _targetOptionFactories = GetResolvable<ITargetPluginOptionsFactory>(); - _validationOptionFactories = GetResolvable<IValidationPluginOptionsFactory>(); - _csrOptionFactories = GetResolvable<ICsrPluginOptionsFactory>(); - _storeOptionFactories = GetResolvable<IStorePluginOptionsFactory>(true); - _installationOptionFactories = GetResolvable<IInstallationPluginOptionsFactory>(true); - - _target = GetResolvable<ITargetPlugin>(); - _validation = GetResolvable<IValidationPlugin>(); - _csr = GetResolvable<ICsrPlugin>(); - _store = GetResolvable<IStorePlugin>(); - _installation = GetResolvable<IInstallationPlugin>(); - - ListPlugins(_target, "target"); - ListPlugins(_validation, "validation"); - ListPlugins(_csr, "csr"); - ListPlugins(_store, "store"); - ListPlugins(_installation, "installation"); + _argumentProviders = GetResolvable<IArgumentsProvider>(); + _optionFactories = GetResolvable<IPluginOptionsFactory>(true); + _plugins = new List<Type>(); + void AddPluginType<T>(string name) + { + var temp = GetResolvable<T>(); + ListPlugins(temp, name); + _plugins.AddRange(temp); + } + AddPluginType<ITargetPlugin>("target"); + AddPluginType<IValidationPlugin>("validation"); + AddPluginType<IOrderPlugin>("order"); + AddPluginType<ICsrPlugin>("csr"); + AddPluginType<IStorePlugin>("store"); + AddPluginType<IInstallationPlugin>("installation"); } private void ListPlugins(IEnumerable<Type> list, string type) @@ -257,5 +207,15 @@ namespace PKISharp.WACS.Services } return ret.ToList(); } + + public T GetFactory<T>(ILifetimeScope scope, string name, string? parameter = null) where T : IPluginOptionsFactory + { + var plugins = GetByName<T>(name, scope); + if (typeof(T) is IValidationPluginOptionsFactory) + { + plugins = plugins.Where(x => string.Equals(parameter, (x as IValidationPluginOptionsFactory)?.ChallengeType, StringComparison.InvariantCultureIgnoreCase)); + } + return plugins.FirstOrDefault(); + } } } diff --git a/src/main.lib/Services/RenewalStoreDisk.cs b/src/main.lib/Services/RenewalStoreDisk.cs index 1931f63..5527a41 100644 --- a/src/main.lib/Services/RenewalStoreDisk.cs +++ b/src/main.lib/Services/RenewalStoreDisk.cs @@ -48,6 +48,7 @@ namespace PKISharp.WACS.Services new StorePluginOptionsConverter(storeConverter), new PluginOptionsConverter<TargetPluginOptions>(_plugin.PluginOptionTypes<TargetPluginOptions>(), _log), new PluginOptionsConverter<CsrPluginOptions>(_plugin.PluginOptionTypes<CsrPluginOptions>(), _log), + new PluginOptionsConverter<OrderPluginOptions>(_plugin.PluginOptionTypes<OrderPluginOptions>(), _log), storeConverter, new PluginOptionsConverter<ValidationPluginOptions>(_plugin.PluginOptionTypes<ValidationPluginOptions>(), _log), new PluginOptionsConverter<InstallationPluginOptions>(_plugin.PluginOptionTypes<InstallationPluginOptions>(), _log) diff --git a/src/main.lib/Services/SettingsService.cs b/src/main.lib/Services/SettingsService.cs index ba3df1a..0fb54c5 100644 --- a/src/main.lib/Services/SettingsService.cs +++ b/src/main.lib/Services/SettingsService.cs @@ -22,8 +22,12 @@ namespace PKISharp.WACS.Services public NotificationSettings Notification { get; private set; } = new NotificationSettings(); public SecuritySettings Security { get; private set; } = new SecuritySettings(); public ScriptSettings Script { get; private set; } = new ScriptSettings(); + public TargetSettings Target { get; private set; } = new TargetSettings(); public ValidationSettings Validation { get; private set; } = new ValidationSettings(); + public OrderSettings Order { get; private set; } = new OrderSettings(); + public CsrSettings Csr { get; private set; } = new CsrSettings(); public StoreSettings Store { get; private set; } = new StoreSettings(); + public InstallationSettings Installation { get; private set; } = new InstallationSettings(); public string ExePath { get; private set; } = Process.GetCurrentProcess().MainModule.FileName; public SettingsService(ILogService log, IArgumentsService arguments) @@ -457,9 +461,29 @@ namespace PKISharp.WACS.Services public int Timeout { get; set; } = 600; } + public class TargetSettings + { + /// <summary> + /// Default plugin to select in the Advanced menu + /// in the menu. + public string? DefaultTarget { get; set; } + } + public class ValidationSettings { /// <summary> + /// Default plugin to select in the Advanced menu (if + /// supported for the target), or when nothing is + /// specified on the command line. + /// </summary> + public string? DefaultValidation { get; set; } + + /// <summary> + /// Default plugin type, e.g. HTTP-01 (default), DNS-01, etc. + /// </summary> + public string? DefaultValidationMode { get; set; } + + /// <summary> /// If set to True, it will cleanup the folder structure /// and files it creates under the site for authorization. /// </summary> @@ -503,9 +527,31 @@ namespace PKISharp.WACS.Services public List<string>? DnsServers { get; set; } } - public class StoreSettings + public class OrderSettings + { + /// <summary> + /// Default plugin to select when none is provided through the + /// command line + /// </summary> + public string? DefaultPlugin { get; set; } + } + + public class CsrSettings { /// <summary> + /// Default plugin to select + /// </summary> + public string? DefaultCsr { get; set; } + } + + public class StoreSettings + { + /// <summary> + /// Default plugin(s) to select + /// </summary> + public string? DefaultStore { get; set; } + + /// <summary> /// The certificate store to save the certificates in. If left empty, /// certificates will be installed either in the WebHosting store, /// or if that is not available, the My store (better known as Personal). @@ -539,5 +585,13 @@ namespace PKISharp.WACS.Services /// </summary> public string? DefaultPemFilesPath { get; set; } } + + public class InstallationSettings + { + /// <summary> + /// Default plugin(s) to select + /// </summary> + public string? DefaultInstallation { get; set; } + } } }
\ No newline at end of file diff --git a/src/main.lib/Wacs.cs b/src/main.lib/Wacs.cs index 7cd61d0..8aeca85 100644 --- a/src/main.lib/Wacs.cs +++ b/src/main.lib/Wacs.cs @@ -241,13 +241,11 @@ namespace PKISharp.WACS.Host { Choice.Create<Func<Task>>( () => _renewalCreator.SetupRenewal(RunLevel.Interactive | RunLevel.Simple), - "Create new certificate (simple for IIS)", "N", - @default: allowIIS, - disabled: (!allowIIS, allowIISReason)), + "Create new certificate (default settings)", "N", + @default: true), Choice.Create<Func<Task>>( () => _renewalCreator.SetupRenewal(RunLevel.Interactive | RunLevel.Advanced), - "Create new certificate (full options)", "M", - @default: !allowIIS), + "Create new certificate (full options)", "M"), Choice.Create<Func<Task>>( () => _renewalManager.CheckRenewals(RunLevel.Interactive), $"Run scheduled renewals ({due} currently due)", "R", diff --git a/src/main.test/Mock/Clients/IISClient.cs b/src/main.test/Mock/Clients/IISClient.cs index 6b8cc38..df0c1a8 100644 --- a/src/main.test/Mock/Clients/IISClient.cs +++ b/src/main.test/Mock/Clients/IISClient.cs @@ -121,7 +121,7 @@ namespace PKISharp.WACS.UnitTests.Mock.Clients public void UpdateBinding(MockSite site, MockBinding binding, BindingOptions bindingOptions) { - site.Bindings.Remove(binding); + _ = site.Bindings.Remove(binding); var updateOptions = bindingOptions .WithHost(binding.Host) .WithIP(binding.IP) diff --git a/src/main.test/Mock/Services/CertificateService.cs b/src/main.test/Mock/Services/CertificateService.cs index aafb218..690c4dc 100644 --- a/src/main.test/Mock/Services/CertificateService.cs +++ b/src/main.test/Mock/Services/CertificateService.cs @@ -3,6 +3,7 @@ using PKISharp.WACS.DomainObjects; using PKISharp.WACS.Plugins.Interfaces; using PKISharp.WACS.Services; using System; +using System.Collections.Generic; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; @@ -11,18 +12,18 @@ namespace PKISharp.WACS.UnitTests.Mock.Services { internal class CertificateService : ICertificateService { - public CertificateInfo? CachedInfo(Renewal renewal, Target? target = null) => null; - public string CacheKey(Renewal renewal, Target target) => ""; - + public CertificateInfo? CachedInfo(Order order) => null; + public IEnumerable<CertificateInfo> CachedInfos(Renewal renewal) => new List<CertificateInfo>(); + public string CacheKey(Order order) => ""; public void Delete(Renewal renewal) {} public void Encrypt() { } - public Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, RunLevel runLevel, Renewal renewal, Target target, OrderDetails order) + public Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, RunLevel runLevel, Order order) { // Create self-signed certificate var ecdsa = ECDsa.Create(); // generate asymmetric key pair - var req = new CertificateRequest($"CN={target.CommonName}", ecdsa, HashAlgorithmName.SHA256); + var req = new CertificateRequest($"CN={order.Target.CommonName}", ecdsa, HashAlgorithmName.SHA256); var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5)); return Task.FromResult(new CertificateInfo(cert) { @@ -32,6 +33,6 @@ namespace PKISharp.WACS.UnitTests.Mock.Services }); } - public Task RevokeCertificate(Renewal renewal) => Task.CompletedTask; + public Task RevokeCertificate(Renewal renewal) => Task.CompletedTask; } } diff --git a/src/main.test/Mock/Services/SettingsService.cs b/src/main.test/Mock/Services/SettingsService.cs index bc1f4c8..032654f 100644 --- a/src/main.test/Mock/Services/SettingsService.cs +++ b/src/main.test/Mock/Services/SettingsService.cs @@ -15,8 +15,12 @@ namespace PKISharp.WACS.UnitTests.Mock.Services public SettingsService.NotificationSettings Notification => new SettingsService.NotificationSettings(); public SettingsService.SecuritySettings Security => new SettingsService.SecuritySettings(); public SettingsService.ClientSettings Client => new SettingsService.ClientSettings(); + public SettingsService.TargetSettings Target => new SettingsService.TargetSettings(); public SettingsService.ValidationSettings Validation => new SettingsService.ValidationSettings(); + public SettingsService.OrderSettings Order => new SettingsService.OrderSettings(); + public SettingsService.CsrSettings Csr => new SettingsService.CsrSettings(); public SettingsService.StoreSettings Store => new SettingsService.StoreSettings(); + public SettingsService.InstallationSettings Installation => new SettingsService.InstallationSettings(); public string ExePath => Process.GetCurrentProcess().MainModule.FileName; public SettingsService.ScriptSettings Script => new SettingsService.ScriptSettings(); } diff --git a/src/main.test/Tests/BindingTests/Bindings.cs b/src/main.test/Tests/BindingTests/Bindings.cs index ac4a157..8b4a57a 100644 --- a/src/main.test/Tests/BindingTests/Bindings.cs +++ b/src/main.test/Tests/BindingTests/Bindings.cs @@ -171,16 +171,16 @@ namespace PKISharp.WACS.UnitTests.Tests.BindingTests Assert.AreEqual(existingBindings.Count() + expectedNew, httpOnlySite.Bindings.Count); var newBindings = httpOnlySite.Bindings.Except(existingBindings); - newBindings.All(newBinding => - { - Assert.AreEqual(existingHost, newBinding.Host); - Assert.AreEqual("https", newBinding.Protocol); - Assert.AreEqual(storeName, newBinding.CertificateStoreName); - Assert.AreEqual(newCert, newBinding.CertificateHash); - Assert.AreEqual(bindingPort, newBinding.Port); - Assert.AreEqual(expectedFlags, newBinding.SSLFlags); - return true; - }); + _ = newBindings.All(newBinding => + { + Assert.AreEqual(existingHost, newBinding.Host); + Assert.AreEqual("https", newBinding.Protocol); + Assert.AreEqual(storeName, newBinding.CertificateStoreName); + Assert.AreEqual(newCert, newBinding.CertificateHash); + Assert.AreEqual(bindingPort, newBinding.Port); + Assert.AreEqual(expectedFlags, newBinding.SSLFlags); + return true; + }); var oldips = existingBindings.Select(x => x.IP).Distinct(); var newips = newBindings.Select(x => x.IP).Distinct(); @@ -541,7 +541,7 @@ namespace PKISharp.WACS.UnitTests.Tests.BindingTests } [TestMethod] - public void UpdateOutOfScope() + public void UpdateOutOfScopeCatchAll() { var iis = new MockIISClient(log) { @@ -566,7 +566,7 @@ namespace PKISharp.WACS.UnitTests.Tests.BindingTests new MockBinding() { IP = DefaultIP, Port = DefaultPort, - Host = outofscopeHost, + Host = "", Protocol = "https", CertificateHash = scopeCert, CertificateStoreName = DefaultStore, @@ -594,6 +594,59 @@ namespace PKISharp.WACS.UnitTests.Tests.BindingTests } [TestMethod] + public void UpdateOutOfScopeRegular() + { + var iis = new MockIISClient(log) + { + MockSites = new[] { + new MockSite() { + Id = inscopeId, + Bindings = new List<MockBinding> { + new MockBinding() { + IP = DefaultIP, + Port = DefaultPort, + Host = inscopeHost, + Protocol = "https", + CertificateHash = scopeCert, + CertificateStoreName = DefaultStore, + SSLFlags = SSLFlags.SNI + } + } + }, + new MockSite() { + Id = outofscopeId, + Bindings = new List<MockBinding> { + new MockBinding() { + IP = DefaultIP, + Port = DefaultPort, + Host = outofscopeHost, + Protocol = "https", + CertificateHash = scopeCert, + CertificateStoreName = DefaultStore, + SSLFlags = SSLFlags.SNI + } + } + } + } + }; + + var bindingOptions = new BindingOptions(). + WithSiteId(inscopeId). + WithIP(DefaultIP). + WithPort(DefaultPort). + WithStore(DefaultStore). + WithThumbprint(newCert); + + var outofScopeSite = iis.GetWebSite(outofscopeId); + iis.AddOrUpdateBindings(new[] { regularHost }, bindingOptions, scopeCert); + Assert.AreEqual(outofScopeSite.Bindings.Count, 1); + + var updatedBinding = outofScopeSite.Bindings[0]; + Assert.AreEqual(DefaultStore, updatedBinding.CertificateStoreName); + Assert.AreEqual(scopeCert, updatedBinding.CertificateHash); + } + + [TestMethod] [DataRow("a.b.c.com", new string[] { }, "a.b.c.com", SSLFlags.None)] [DataRow("a.b.c.com", new[] { "a.b.c.com" }, "*.b.c.com", SSLFlags.None)] [DataRow("a.b.c.com", new[] { "a.b.c.com", "*.b.c.com" }, "*.c.com", SSLFlags.None)] diff --git a/src/main.test/Tests/EcnryptionTests/EncryptionTest.cs b/src/main.test/Tests/EcnryptionTests/EncryptionTest.cs new file mode 100644 index 0000000..d370eb0 --- /dev/null +++ b/src/main.test/Tests/EcnryptionTests/EncryptionTest.cs @@ -0,0 +1,33 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PKISharp.WACS.Services.Serialization; + +namespace PKISharp.WACS.UnitTests.Tests.EcnryptionTests +{ + [TestClass] + public class EncryptionTests + { + [TestMethod] + public void TurnOnOff() + { + // Create encrypted value + var plain = "---BLA---"; + var plainString = new ProtectedString(plain); + var encrypted = plainString.DiskValue(true); + Assert.IsTrue(encrypted != null); + + // Read back + var log = new Mock.Services.LogService(false); + var readBack = new ProtectedString(encrypted ?? "", log); + Assert.AreEqual(plain, readBack.Value); + + // Turn off encryption + var turnOff = new ProtectedString(encrypted ?? "", log); + var turnOffValue = turnOff.DiskValue(false); + Assert.IsTrue(turnOffValue != null); + + // Read back turned off value + var readBack2 = new ProtectedString(turnOffValue ?? "", log); + Assert.AreEqual(readBack2.Value, plain); + } + } +} diff --git a/src/main.test/Tests/InstallationPluginTests/MultipleInstallerTests.cs b/src/main.test/Tests/InstallationPluginTests/MultipleInstallerTests.cs index 331f241..8c8aa79 100644 --- a/src/main.test/Tests/InstallationPluginTests/MultipleInstallerTests.cs +++ b/src/main.test/Tests/InstallationPluginTests/MultipleInstallerTests.cs @@ -12,6 +12,7 @@ using mock = PKISharp.WACS.UnitTests.Mock.Services; using System; using System.Collections.Generic; using System.Threading.Tasks; +using PKISharp.WACS.Clients.DNS; namespace PKISharp.WACS.UnitTests.Tests.InstallationPluginTests { @@ -20,11 +21,13 @@ namespace PKISharp.WACS.UnitTests.Tests.InstallationPluginTests { private readonly ILogService log; private readonly mock.MockPluginService plugins; + private readonly mock.MockSettingsService settings; public MultipleInstallerTests() { log = new mock.LogService(false); plugins = new mock.MockPluginService(log); + settings = new mock.MockSettingsService(); } /// <summary> @@ -40,23 +43,30 @@ namespace PKISharp.WACS.UnitTests.Tests.InstallationPluginTests var builder = new ContainerBuilder(); - builder.RegisterInstance(plugins). + _ = builder.RegisterType<LookupClientProvider>(); + _ = builder.RegisterType<ProxyService>(); + _ = builder.RegisterType<DomainParseService>(); + _ = builder.RegisterType<IISHelper>(); + _ = builder.RegisterInstance(plugins). As<IPluginService>(). SingleInstance(); - builder.RegisterInstance(log). + _ = builder.RegisterInstance(settings). + As<ISettingsService>(). + SingleInstance(); + _ = builder.RegisterInstance(log). As<ILogService>(). SingleInstance(); - builder.RegisterType<Mock.Clients.MockIISClient>(). + _ = builder.RegisterType<Mock.Clients.MockIISClient>(). As<IIISClient>(). SingleInstance(); - builder.RegisterType<ArgumentsParser>(). + _ = builder.RegisterType<ArgumentsParser>(). SingleInstance(). WithParameter(new TypedParameter(typeof(string[]), commandLine.Split(' '))); - builder.RegisterType<ArgumentsService>(). + _ = builder.RegisterType<ArgumentsService>(). As<IArgumentsService>(). SingleInstance(); - builder.RegisterType<mock.UserRoleService>().As<IUserRoleService>().SingleInstance(); - builder.RegisterType<UnattendedResolver>().As<IResolver>(); + _ = builder.RegisterType<mock.UserRoleService>().As<IUserRoleService>().SingleInstance(); + _ = builder.RegisterType<UnattendedResolver>().As<IResolver>(); plugins.Configure(builder); var scope = builder.Build(); diff --git a/src/main.test/Tests/InstallationPluginTests/ScriptPluginTests.cs b/src/main.test/Tests/InstallationPluginTests/ScriptPluginTests.cs index 7a6746b..aefe164 100644 --- a/src/main.test/Tests/InstallationPluginTests/ScriptPluginTests.cs +++ b/src/main.test/Tests/InstallationPluginTests/ScriptPluginTests.cs @@ -66,8 +66,10 @@ namespace PKISharp.WACS.UnitTests.Tests.InstallationPluginTests var settings = new MockSettingsService(); var userRoleService = new Mock.Services.UserRoleService(); var store = new CertificateStore(log, iis, settings, userRoleService, new FindPrivateKey(log), storeOptions); - var oldCert = cs.RequestCertificate(null, RunLevel.Unattended, renewal, new Target("", "test.local", new List<TargetPart>()), new ACMESharp.Protocol.OrderDetails()).Result; - var newCert = cs.RequestCertificate(null, RunLevel.Unattended, renewal, new Target("", "test.local", new List<TargetPart>()), new ACMESharp.Protocol.OrderDetails()).Result; + var target = new Target("", "test.local", new List<TargetPart>()); + var targetOrder = new Order(renewal, target); + var oldCert = cs.RequestCertificate(null, RunLevel.Unattended, targetOrder).Result; + var newCert = cs.RequestCertificate(null, RunLevel.Unattended, targetOrder).Result; newCert.StoreInfo.Add(typeof(CertificateStore), new StoreInfo() { }); var options = new ScriptOptions { @@ -75,7 +77,7 @@ namespace PKISharp.WACS.UnitTests.Tests.InstallationPluginTests ScriptParameters = parameters }; var installer = new Script(renewal, options, new Clients.ScriptClient(log, settings)); - installer.Install(new[] { store }, newCert, oldCert).Wait(); + installer.Install(target, new[] { store }, newCert, oldCert).Wait(); } [TestMethod] diff --git a/src/main.test/Tests/TargetPluginTests/IISBindingTests.cs b/src/main.test/Tests/TargetPluginTests/IISBindingTests.cs index 72760d2..19c2e64 100644 --- a/src/main.test/Tests/TargetPluginTests/IISBindingTests.cs +++ b/src/main.test/Tests/TargetPluginTests/IISBindingTests.cs @@ -34,7 +34,7 @@ namespace PKISharp.WACS.UnitTests.Tests.TargetPluginTests { var optionsParser = new ArgumentsParser(log, plugins, commandLine.Split(' ')); var arguments = new ArgumentsService(log, optionsParser); - var x = new IISOptionsFactory(log, iis, helper, arguments, userRoleService); + var x = new IISOptionsFactory(log, helper, arguments, userRoleService); return x.Default().Result; } diff --git a/src/main.test/Tests/TargetPluginTests/IISSiteTests.cs b/src/main.test/Tests/TargetPluginTests/IISSiteTests.cs index 6ae2d28..5d0fc5a 100644 --- a/src/main.test/Tests/TargetPluginTests/IISSiteTests.cs +++ b/src/main.test/Tests/TargetPluginTests/IISSiteTests.cs @@ -35,7 +35,7 @@ namespace PKISharp.WACS.UnitTests.Tests.TargetPluginTests { var optionsParser = new ArgumentsParser(log, plugins, commandLine.Split(' ')); var arguments = new ArgumentsService(log, optionsParser); - var x = new IISOptionsFactory(log, iis, helper, arguments, userRoleService); + var x = new IISOptionsFactory(log, helper, arguments, userRoleService); return x.Default().Result; } diff --git a/src/main.test/Tests/TargetPluginTests/IISSitesTests.cs b/src/main.test/Tests/TargetPluginTests/IISSitesTests.cs index 71ac3b3..95fe43f 100644 --- a/src/main.test/Tests/TargetPluginTests/IISSitesTests.cs +++ b/src/main.test/Tests/TargetPluginTests/IISSitesTests.cs @@ -34,7 +34,7 @@ namespace PKISharp.WACS.UnitTests.Tests.TargetPluginTests { var optionsParser = new ArgumentsParser(log, plugins, commandLine.Split(' ')); var arguments = new ArgumentsService(log, optionsParser); - var x = new IISOptionsFactory(log, iis, helper, arguments, userRoleService); + var x = new IISOptionsFactory(log, helper, arguments, userRoleService); return x.Default().Result; } diff --git a/src/main.test/Tests/TargetPluginTests/IISTests.cs b/src/main.test/Tests/TargetPluginTests/IISTests.cs index 252060d..c9b79af 100644 --- a/src/main.test/Tests/TargetPluginTests/IISTests.cs +++ b/src/main.test/Tests/TargetPluginTests/IISTests.cs @@ -6,8 +6,6 @@ using PKISharp.WACS.Extensions; using PKISharp.WACS.Plugins.TargetPlugins; using PKISharp.WACS.Services; using PKISharp.WACS.UnitTests.Mock.Services; -using System; -using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -35,7 +33,7 @@ namespace PKISharp.WACS.UnitTests.Tests.TargetPluginTests { var optionsParser = new ArgumentsParser(log, plugins, commandLine.Split(' ')); var arguments = new ArgumentsService(log, optionsParser); - var x = new IISOptionsFactory(log, iis, helper, arguments, userRoleService); + var x = new IISOptionsFactory(log, helper, arguments, userRoleService); return x.Default().Result; } diff --git a/src/main.test/Tests/TargetPluginTests/PatternTests.cs b/src/main.test/Tests/TargetPluginTests/PatternTests.cs new file mode 100644 index 0000000..4a3f453 --- /dev/null +++ b/src/main.test/Tests/TargetPluginTests/PatternTests.cs @@ -0,0 +1,94 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PKISharp.WACS.Extensions; +using System.Text.RegularExpressions; + +namespace PKISharp.WACS.UnitTests.Tests.TargetPluginTests +{ + [TestClass] + public class PatternTests + { + public PatternTests() + { + } + + public void RegexPattern(string input, string match, bool shouldSucceed) + { + var pattern = input.PatternToRegex(); + var regex = new Regex(pattern); + Assert.AreEqual(regex.Match(match).Success, shouldSucceed); + } + + [DataRow("e?", "ee", true)] + [DataRow("e?", "eee", false)] + [DataRow("e?", "e?", true)] + [TestMethod] + public void RegularQuestion(string input, string match, bool shouldSucceed) => RegexPattern(input, match, shouldSucceed); + + [DataRow("e?", "ee", false)] + [DataRow("e?", "eee", false)] + [DataRow("e?", "e?", true)] + [TestMethod] + public void EscapeQuestion(string input, string match, bool shouldSucceed) => RegexPattern(input.EscapePattern(), match, shouldSucceed); + + [DataRow("e*", "ee", true)] + [DataRow("e*", "eee", true)] + [DataRow("e*", "fee", false)] + [DataRow("e*", "e*", true)] + [TestMethod] + public void RegularStar(string input, string match, bool shouldSucceed) => RegexPattern(input, match, shouldSucceed); + + [DataRow("e*", "ee", false)] + [DataRow("e*", "eee", false)] + [DataRow("e*", "fee", false)] + [DataRow("e*", "e*", true)] + [TestMethod] + public void EscapeStar(string input, string match, bool shouldSucceed) => RegexPattern(input.EscapePattern(), match, shouldSucceed); + + [DataRow("e\\?", "e?", true)] + [DataRow("e\\?", "e?e", false)] + [DataRow("e\\?", "ee", false)] + [DataRow("e\\?", "e\\?", false)] + [TestMethod] + public void EscapedQuestion(string input, string match, bool shouldSucceed) => RegexPattern(input, match, shouldSucceed); + + [DataRow("e\\?", "e?", false)] + [DataRow("e\\?", "e?e", false)] + [DataRow("e\\?", "ee", false)] + [DataRow("e\\?", "e\\?", true)] + [TestMethod] + public void DoubleEscapedQuestion(string input, string match, bool shouldSucceed) => RegexPattern(input.EscapePattern(), match, shouldSucceed); + + [DataRow("e\\*", "e*e", false)] + [DataRow("e\\*", "ee", false)] + [DataRow("e\\*", "e*", true)] + [DataRow("e\\*", "e\\*", false)] + [TestMethod] + public void EscapedStar(string input, string match, bool shouldSucceed) => RegexPattern(input, match, shouldSucceed); + + [DataRow("e\\*", "e*e", false)] + [DataRow("e\\*", "ee", false)] + [DataRow("e\\*", "e*", false)] + [DataRow("e\\*", "e\\*", true)] + [TestMethod] + public void DoubleEscapedStar(string input, string match, bool shouldSucceed) => RegexPattern(input.EscapePattern(), match, shouldSucceed); + + [DataRow("e\\\\*", "e\\*e", true)] + [DataRow("e\\\\*", "ee", false)] + [DataRow("e\\\\*", "e*", false)] + [DataRow("e\\\\*", "e\\\\*", true)] + [TestMethod] + public void EscapedSlash(string input, string match, bool shouldSucceed) => RegexPattern(input, match, shouldSucceed); + + [DataRow("e\\\\*", "e\\*e", false)] + [DataRow("e\\\\*", "ee", false)] + [DataRow("e\\\\*", "e*", false)] + [DataRow("e\\\\*", "e\\\\*", true)] + [TestMethod] + public void DoubleEscapedSlash(string input, string match, bool shouldSucceed) => RegexPattern(input.EscapePattern(), match, shouldSucceed); + + [DataRow("e\\\\\\*", "e\\*", true)] + [DataRow("e\\\\\\*", "e\\?", false)] + [TestMethod] + public void EscapedSlashStar(string input, string match, bool shouldSucceed) => RegexPattern(input, match, shouldSucceed); + } +}
\ No newline at end of file diff --git a/src/main/settings.json b/src/main/settings.json index e8ebd59..7657b14 100644 --- a/src/main/settings.json +++ b/src/main/settings.json @@ -55,7 +55,12 @@ "Script": { "Timeout": 600 }, + "Target": { + "DefaultTarget": null + }, "Validation": { + "DefaultValidation": "script", + "DefaultValidationMode": "dns-01", "CleanupFolders": true, "PreValidateDns": true, "PreValidateDnsRetryCount": 5, @@ -63,10 +68,20 @@ "AllowDnsSubstitution": true, "DnsServers": [ "8.8.8.8", "1.1.1.1", "8.8.4.4" ] }, + "Order": { + "DefaultOrder": null + }, + "Csr": { + "DefaultCsr": null + }, "Store": { + "DefaultStore": null, "DefaultCertificateStore": null, "DefaultCentralSslStore": null, "DefaultCentralSslPfxPassword": null, "DefaultPemFilesPath": null + }, + "Installation": { + "DefaultInstallation": null } }
\ No newline at end of file |