diff options
Diffstat (limited to 'src/main.lib/Plugins')
31 files changed, 641 insertions, 456 deletions
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> |