diff options
Diffstat (limited to 'src')
48 files changed, 955 insertions, 577 deletions
diff --git a/src/fluent-command-line-parser b/src/fluent-command-line-parser -Subproject 47edfba7969d1c3a948b580e568b24021d86958 +Subproject 6864598368f667c8b4e44a1c056e7ce7314120d diff --git a/src/main.lib/Clients/Acme/AcmeClient.cs b/src/main.lib/Clients/Acme/AcmeClient.cs index dcc0822..c13dda8 100644 --- a/src/main.lib/Clients/Acme/AcmeClient.cs +++ b/src/main.lib/Clients/Acme/AcmeClient.cs @@ -213,6 +213,25 @@ namespace PKISharp.WACS.Clients.Acme } /// <summary> + /// Test the network connection + /// </summary> + internal async Task CheckNetwork() + { + var httpClient = _proxyService.GetHttpClient(); + httpClient.BaseAddress = _settings.BaseUri; + try + { + _ = await httpClient.GetStringAsync("directory"); + _log.Debug("Connection OK!"); + } + catch (Exception ex) + { + _log.Error(ex, "Error connecting to ACME server"); + } + + } + + /// <summary> /// Get contact information /// </summary> /// <returns></returns> @@ -444,7 +463,7 @@ namespace PKISharp.WACS.Clients.Acme /// <typeparam name="T"></typeparam> /// <param name="executor"></param> /// <returns></returns> - private async Task<T> Retry<T>(Func<Task<T>> executor) + private async Task<T> Retry<T>(Func<Task<T>> executor, int attempt = 0) { try { @@ -452,12 +471,12 @@ namespace PKISharp.WACS.Clients.Acme } catch (AcmeProtocolException apex) { - if (apex.ProblemType == ProblemType.BadNonce) + if (attempt < 3 && apex.ProblemType == ProblemType.BadNonce) { _log.Warning("First chance error calling into ACME server, retrying with new nonce..."); var client = await GetClient(); await client.GetNonceAsync(); - return await executor(); + return await Retry(executor, attempt += 1); } else { diff --git a/src/main.lib/Clients/AcmeDnsClient.cs b/src/main.lib/Clients/AcmeDnsClient.cs index 7a28f1a..ac862f1 100644 --- a/src/main.lib/Clients/AcmeDnsClient.cs +++ b/src/main.lib/Clients/AcmeDnsClient.cs @@ -73,7 +73,7 @@ namespace PKISharp.WACS.Clients _input.Show("Type", "CNAME"); _input.Show("Content", newReg.Fulldomain + "."); _input.Show("Note", "Some DNS control panels add the final dot automatically. Only one is required."); - if (!await _input.Wait("Please press enter after you've created and verified the record")) + if (!await _input.Wait("Please press <Enter> after you've created and verified the record")) { throw new Exception("User aborted"); } diff --git a/src/main.lib/Clients/DNS/LookupClientProvider.cs b/src/main.lib/Clients/DNS/LookupClientProvider.cs index a08559e..061eac7 100644 --- a/src/main.lib/Clients/DNS/LookupClientProvider.cs +++ b/src/main.lib/Clients/DNS/LookupClientProvider.cs @@ -96,7 +96,6 @@ namespace PKISharp.WACS.Clients.DNS _lookupClients.Add( key, new LookupClientWrapper( - _domainParser, _log, ip.Equals(new IPAddress(0)) ? null : ip, this)); diff --git a/src/main.lib/Clients/DNS/LookupClientWrapper.cs b/src/main.lib/Clients/DNS/LookupClientWrapper.cs index bf97254..dac01c7 100644 --- a/src/main.lib/Clients/DNS/LookupClientWrapper.cs +++ b/src/main.lib/Clients/DNS/LookupClientWrapper.cs @@ -2,6 +2,7 @@ using DnsClient.Protocol; using PKISharp.WACS.Services; using Serilog.Context; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -12,32 +13,29 @@ namespace PKISharp.WACS.Clients.DNS public class LookupClientWrapper { private readonly ILogService _log; - private readonly DomainParseService _domainParser; private readonly LookupClientProvider _provider; private readonly IPAddress? _ipAddress; public ILookupClient LookupClient { get; private set; } public string IpAddress => _ipAddress?.ToString() ?? "[System]"; - public LookupClientWrapper(DomainParseService domainParser, ILogService logService, IPAddress? ipAddress, LookupClientProvider provider) + public LookupClientWrapper(ILogService logService, IPAddress? ipAddress, LookupClientProvider provider) { _ipAddress = ipAddress; LookupClient = ipAddress == null ? new LookupClient() : new LookupClient(ipAddress); LookupClient.UseCache = false; _log = logService; - _domainParser = domainParser; _provider = provider; } - public string GetRootDomain(string domainName) => _domainParser.GetTLD(domainName.TrimEnd('.')); - public async Task<IEnumerable<IPAddress>?> GetAuthoritativeNameServers(string domainName, int round) { domainName = domainName.TrimEnd('.'); _log.Debug("Querying name servers for {part}", domainName); var nsResponse = await LookupClient.QueryAsync(domainName, QueryType.NS); var nsRecords = nsResponse.Answers.NsRecords(); - if (!nsRecords.Any()) + var cnameRecords = nsResponse.Answers.CnameRecords(); + if (!nsRecords.Any() && !cnameRecords.Any()) { nsRecords = nsResponse.Authorities.OfType<NsRecord>(); } diff --git a/src/main.lib/Clients/IIS/IISHelper.cs b/src/main.lib/Clients/IIS/IISHelper.cs index d4fb1be..63a8649 100644 --- a/src/main.lib/Clients/IIS/IISHelper.cs +++ b/src/main.lib/Clients/IIS/IISHelper.cs @@ -186,12 +186,6 @@ namespace PKISharp.WACS.Clients.IIS || regex.IsMatch(binding.HostPunycode); } - internal string PatternToRegex(string pattern) - { - var parts = pattern.ParseCsv(); - return $"^({string.Join('|', parts.Select(x => Regex.Escape(x).Replace(@"\*", ".*").Replace(@"\?", ".")))})$"; - } - internal string HostsToRegex(IEnumerable<string> hosts) => $"^({string.Join('|', hosts.Select(x => Regex.Escape(x)))})$"; @@ -199,7 +193,7 @@ namespace PKISharp.WACS.Clients.IIS { if (!string.IsNullOrEmpty(options.IncludePattern)) { - return new Regex(PatternToRegex(options.IncludePattern)); + return new Regex(options.IncludePattern.PatternToRegex()); } if (options.IncludeHosts != null && options.IncludeHosts.Any()) { diff --git a/src/main.lib/Clients/ScriptClient.cs b/src/main.lib/Clients/ScriptClient.cs index bf1961d..0d0e4f6 100644 --- a/src/main.lib/Clients/ScriptClient.cs +++ b/src/main.lib/Clients/ScriptClient.cs @@ -27,7 +27,7 @@ namespace PKISharp.WACS.Clients if (actualScript.EndsWith(".ps1")) { actualScript = "powershell.exe"; - actualParameters = $"-executionpolicy bypass &'{script}' {parameters.Replace("\"", "\"\"\"")}"; + actualParameters = $"-windowstyle hidden -noninteractive -executionpolicy bypass .'{script}' {parameters.Replace("\"", "\"\"\"")}"; } var PSI = new ProcessStartInfo(actualScript) { @@ -80,7 +80,7 @@ namespace PKISharp.WACS.Clients process.EnableRaisingEvents = true; process.Exited += (s, e) => { - _log.Information(LogType.Event, output.ToString()); + _log.Information(LogType.Event | LogType.Disk, output.ToString()); exited = true; if (process.ExitCode != 0) { @@ -102,6 +102,7 @@ namespace PKISharp.WACS.Clients process.BeginErrorReadLine(); process.BeginOutputReadLine(); + process.StandardInput.Close(); // Helps end the process var totalWait = 0; var interval = 2000; while (!exited && totalWait < _settings.Script.Timeout * 1000) diff --git a/src/main.lib/Configuration/MainArguments.cs b/src/main.lib/Configuration/MainArguments.cs index 7dee7e9..ba6ae22 100644 --- a/src/main.lib/Configuration/MainArguments.cs +++ b/src/main.lib/Configuration/MainArguments.cs @@ -22,7 +22,7 @@ namespace PKISharp.WACS.Configuration public string? Id { get; set; } public string? FriendlyName { get; set; } public bool Cancel { get; set; } - + public bool Revoke { get; set; } public string? Target { get; set; } public string? Validation { get; set; } public string? ValidationMode { get; set; } diff --git a/src/main.lib/Configuration/MainArgumentsProvider.cs b/src/main.lib/Configuration/MainArgumentsProvider.cs index 7bbe8b7..2f02081 100644 --- a/src/main.lib/Configuration/MainArgumentsProvider.cs +++ b/src/main.lib/Configuration/MainArgumentsProvider.cs @@ -1,4 +1,5 @@ using Fclp; +using PKISharp.WACS.Plugins.TargetPlugins; namespace PKISharp.WACS.Configuration { @@ -58,13 +59,16 @@ namespace PKISharp.WACS.Configuration .WithDescription("Renew any certificates that are due. This argument is used by the scheduled task. Note that it's not possible to change certificate properties and renew at the same time."); parser.Setup(o => o.Force) .As("force") - .WithDescription("Force renewal on all scheduled certificates when used together with --renew. Otherwise just bypasses the certificate cache on new certificate requests."); + .WithDescription("Force renewal when used together with --renew. Otherwise bypasses the certificate cache on new certificate requests."); // Commands parser.Setup(o => o.Cancel) - .As("cancel") - .WithDescription("Cancel scheduled renewal specified by the friendlyname argument."); + .As("cancel") + .WithDescription("Cancel renewal specified by the --friendlyname or --id arguments."); + parser.Setup(o => o.Revoke) + .As("revoke") + .WithDescription("Revoke the most recently issued certificate for the renewal specified by the --friendlyname or --id arguments."); parser.Setup(o => o.List) .As("list") @@ -74,11 +78,11 @@ namespace PKISharp.WACS.Configuration parser.Setup(o => o.Id) .As("id") - .WithDescription("[--target|--cancel|--renew] Id of a new or existing renewal, can be used to override the default when creating a new renewal or to specify a specific renewal for other commands."); + .WithDescription("[--target|--cancel|--renew|--revoke] Id of a new or existing renewal, can be used to override the default when creating a new renewal or to specify a specific renewal for other commands."); parser.Setup(o => o.FriendlyName) .As("friendlyname") - .WithDescription("[--target|--cancel|--renew] Friendly name of a new or existing renewal, can be used to override the default when creating a new renewal or to specify a specific renewal for other commands."); + .WithDescription("[--target|--cancel|--renew|--revoke] Friendly name of a new or existing renewal, can be used to override the default when creating a new renewal or to specify a specific renewal for other commands. In the latter case a pattern might be used. " + IISArgumentsProvider.PatternExamples); // Plugins (unattended) diff --git a/src/main.lib/Extensions/StringExtensions.cs b/src/main.lib/Extensions/StringExtensions.cs index 35ea6ea..0040435 100644 --- a/src/main.lib/Extensions/StringExtensions.cs +++ b/src/main.lib/Extensions/StringExtensions.cs @@ -109,5 +109,11 @@ namespace PKISharp.WACS.Extensions return false; } } + + public static string PatternToRegex(this string pattern) + { + var parts = pattern.ParseCsv(); + return $"^({string.Join('|', parts.Select(x => Regex.Escape(x).Replace(@"\*", ".*").Replace(@"\?", ".")))})$"; + } } }
\ No newline at end of file diff --git a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs index 4666ba8..74f3b8b 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullInstallationOptionsFactory.cs @@ -15,8 +15,9 @@ namespace PKISharp.WACS.Plugins.Base.Factories.Null { Type IPluginOptionsFactory.InstanceType => typeof(NullInstallation); Type IPluginOptionsFactory.OptionsType => typeof(NullInstallationOptions); - Task<InstallationPluginOptions> IInstallationPluginOptionsFactory.Aquire(Target target, IInputService inputService, RunLevel runLevel) => Task.FromResult<InstallationPluginOptions>(new NullInstallationOptions()); - Task<InstallationPluginOptions> IInstallationPluginOptionsFactory.Default(Target target) => Task.FromResult<InstallationPluginOptions>(new NullInstallationOptions()); + Task<InstallationPluginOptions> Generate() => Task.FromResult<InstallationPluginOptions>(new NullInstallationOptions()); + Task<InstallationPluginOptions> IInstallationPluginOptionsFactory.Aquire(Target target, IInputService inputService, RunLevel runLevel) => Generate(); + Task<InstallationPluginOptions> IInstallationPluginOptionsFactory.Default(Target target) => Generate(); bool IInstallationPluginOptionsFactory.CanInstall(IEnumerable<Type> storeTypes) => true; int IPluginOptionsFactory.Order => int.MaxValue; bool IPluginOptionsFactory.Disabled => false; @@ -29,7 +30,7 @@ namespace PKISharp.WACS.Plugins.Base.Factories.Null internal class NullInstallationOptions : InstallationPluginOptions<NullInstallation> { public override string Name => "None"; - public override string Description => "Do not run any (extra) installation steps"; + public override string Description => "No (additional) installation steps"; } internal class NullInstallation : IInstallationPlugin diff --git a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs index a75759a..4b018d7 100644 --- a/src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs +++ b/src/main.lib/Plugins/Base/OptionsFactories/Null/NullStoreOptionsFactory.cs @@ -1,4 +1,5 @@ -using PKISharp.WACS.Plugins.Base.Options; +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Plugins.Base.Options; using PKISharp.WACS.Plugins.Interfaces; using PKISharp.WACS.Services; using System; @@ -11,14 +12,39 @@ namespace PKISharp.WACS.Plugins.Base.Factories.Null /// </summary> internal class NullStoreOptionsFactory : IStorePluginOptionsFactory, INull { - Type IPluginOptionsFactory.InstanceType => typeof(object); - Type IPluginOptionsFactory.OptionsType => typeof(object); - Task<StorePluginOptions?> IStorePluginOptionsFactory.Aquire(IInputService inputService, RunLevel runLevel) => Task.FromResult<StorePluginOptions?>(null); - Task<StorePluginOptions?> IStorePluginOptionsFactory.Default() => Task.FromResult<StorePluginOptions?>(null); - string IPluginOptionsFactory.Name => "None"; + 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(); bool IPluginOptionsFactory.Disabled => false; - string IPluginOptionsFactory.Description => "No additional storage steps required"; - bool IPluginOptionsFactory.Match(string name) => false; + string IPluginOptionsFactory.Name => NullStoreOptions.PluginName; + string IPluginOptionsFactory.Description => new NullStoreOptions().Description; + bool IPluginOptionsFactory.Match(string name) => string.Equals(name, new NullInstallationOptions().Name, StringComparison.CurrentCultureIgnoreCase); int IPluginOptionsFactory.Order => int.MaxValue; } + + [Plugin("cfdd7caa-ba34-4e9e-b9de-2a3d64c4f4ec")] + internal class NullStoreOptions : StorePluginOptions<NullStore> + { + internal const string PluginName = "None"; + public override string Name => PluginName; + public override string Description => "No (additional) store steps"; + } + + internal class NullStore : IStorePlugin + { + bool IPlugin.Disabled => false; + public Task Delete(CertificateInfo certificateInfo) => Task.CompletedTask; + public Task Save(CertificateInfo certificateInfo) { + certificateInfo.StoreInfo.Add(GetType(), + new StoreInfo() + { + Name = NullStoreOptions.PluginName, + Path = "" + }); + return Task.CompletedTask; + } + } + } diff --git a/src/main.lib/Plugins/Interfaces/IValidationPlugin.cs b/src/main.lib/Plugins/Interfaces/IValidationPlugin.cs index 3e588f8..ba899db 100644 --- a/src/main.lib/Plugins/Interfaces/IValidationPlugin.cs +++ b/src/main.lib/Plugins/Interfaces/IValidationPlugin.cs @@ -7,7 +7,7 @@ namespace PKISharp.WACS.Plugins.Interfaces /// <summary> /// Instance interface /// </summary> - public interface IValidationPlugin : IDisposable, IPlugin + public interface IValidationPlugin : IPlugin { /// <summary> /// Prepare challenge @@ -17,5 +17,10 @@ namespace PKISharp.WACS.Plugins.Interfaces /// <param name="challenge"></param> /// <returns></returns> Task PrepareChallenge(IChallengeValidationDetails challengeDetails); + + /// <summary> + /// Clean up after validation attempt + /// </summary> + Task CleanUp(); } } diff --git a/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs b/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs index 6af894b..76ca2d9 100644 --- a/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs +++ b/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs @@ -206,7 +206,6 @@ namespace PKISharp.WACS.Plugins.Resolvers { question = "Would you like to store it in another way too?"; defaultType = typeof(NullStoreOptionsFactory); - filtered.Add(new NullStoreOptionsFactory()); } var store = await _input.ChooseOptional( diff --git a/src/main.lib/Plugins/TargetPlugins/IIS/IISArgumentsProvider.cs b/src/main.lib/Plugins/TargetPlugins/IIS/IISArgumentsProvider.cs index 6462bfd..2478723 100644 --- a/src/main.lib/Plugins/TargetPlugins/IIS/IISArgumentsProvider.cs +++ b/src/main.lib/Plugins/TargetPlugins/IIS/IISArgumentsProvider.cs @@ -14,6 +14,7 @@ namespace PKISharp.WACS.Plugins.TargetPlugins "`example.com` (but not `my.example.com`) and the pattern `?.example.com` will match " + "`a.example.com` and `b.example.com` (but not `www.example.com`). Note that multiple patterns " + "can be combined by comma seperating them."; + public override void Configure(FluentCommandLineParser<IISArguments> parser) { parser.Setup(o => o.SiteId) diff --git a/src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs b/src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs index 6954fe8..733f61d 100644 --- a/src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs +++ b/src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs @@ -202,7 +202,7 @@ namespace PKISharp.WACS.Plugins.TargetPlugins await ListBindings(input, runLevel, filtered); input.Show(null, "The listed bindings match your current filter settings. " + "If you wish to exclude one or more of them from the certificate, please " + - "input those bindings now. Press <ENTER> to include all listed bindings.", true); + "input those bindings now. Press <Enter> to include all listed bindings.", true); await InputHosts("Exclude bindings", input, allBindings, filtered, options, () => options.ExcludeHosts, x => options.ExcludeHosts = x); @@ -337,7 +337,7 @@ namespace PKISharp.WACS.Plugins.TargetPlugins } try { - var regexString = _iisHelper.PatternToRegex(pattern); + var regexString = pattern.PatternToRegex(); var actualRegex = new Regex(regexString); ret.IncludePattern = pattern; return true; @@ -410,7 +410,7 @@ namespace PKISharp.WACS.Plugins.TargetPlugins } else { - return default(ConsoleColor?); + return default; } } diff --git a/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/AcmeOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/AcmeOptionsFactory.cs index b437b5f..04cf9c8 100644 --- a/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/AcmeOptionsFactory.cs +++ b/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/AcmeOptionsFactory.cs @@ -53,11 +53,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns var identifiers = target.Parts.SelectMany(x => x.Identifiers).Distinct(); foreach (var identifier in identifiers) { - if (!await acmeDnsClient.EnsureRegistration(identifier.Replace("*.", ""), true)) - { - // Something failed or was aborted - return null; - } + await acmeDnsClient.EnsureRegistration(identifier.Replace("*.", ""), true); } return ret; } @@ -92,13 +88,13 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns { if (!await acmeDnsClient.EnsureRegistration(identifier.Replace("*.", ""), false)) { + _log.Warning("No (valid) acme-dns registration could be found for {identifier}.", identifier); valid = false; } } if (!valid) { - _log.Error($"Setting up this certificate is not possible in unattended mode because no (valid) acme-dns registration could be found for one or more of the specified domains."); - return null; + _log.Warning($"Creating his renewal might fail because the acme-dns configuration for one or more identifiers looks unhealthy."); } return ret; } diff --git a/src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs b/src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs index 72d0f01..845bb0e 100644 --- a/src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs +++ b/src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs @@ -66,32 +66,24 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins try { var dnsClients = await _dnsClientProvider.GetClients(Challenge.DnsRecordName, attempt); - - _log.Debug("Preliminary validation will now check name servers: {address}", - string.Join(", ", dnsClients.Select(x => x.IpAddress))); - - // Parallel queries - var answers = await Task.WhenAll(dnsClients.Select(client => client.GetTextRecordValues(Challenge.DnsRecordName, attempt))); - - // Loop through results - for (var i = 0; i < dnsClients.Count(); i++) + foreach (var client in dnsClients) { - var currentClient = dnsClients[i]; - var currentResult = answers[i]; - if (!currentResult.Any()) + _log.Debug("Preliminary validation will now check name server {ip}", client.IpAddress); + var answers = await client.GetTextRecordValues(Challenge.DnsRecordName, attempt); + if (!answers.Any()) { - _log.Warning("Preliminary validation at {address} failed: no TXT records found", currentClient.IpAddress); + _log.Warning("Preliminary validation at {address} failed: no TXT records found", client.IpAddress); return false; } - if (!currentResult.Contains(Challenge.DnsRecordValue)) + if (!answers.Contains(Challenge.DnsRecordValue)) { - _log.Warning("Preliminary validation at {address} failed: {ExpectedTxtRecord} not found in {TxtRecords}", - currentClient.IpAddress, - Challenge.DnsRecordValue, - string.Join(", ", currentResult)); + _log.Warning("Preliminary validation at {address} failed: {ExpectedTxtRecord} not found in {TxtRecords}", + client.IpAddress, + Challenge.DnsRecordValue, + string.Join(", ", answers)); return false; } - _log.Debug("Preliminary validation at {address} looks good!", currentClient.IpAddress); + _log.Debug("Preliminary validation at {address} looks good!", client.IpAddress); } } catch (Exception ex) diff --git a/src/main.lib/Plugins/ValidationPlugins/Dns/Manual/Manual.cs b/src/main.lib/Plugins/ValidationPlugins/Dns/Manual/Manual.cs index d9fc670..317afe9 100644 --- a/src/main.lib/Plugins/ValidationPlugins/Dns/Manual/Manual.cs +++ b/src/main.lib/Plugins/ValidationPlugins/Dns/Manual/Manual.cs @@ -32,7 +32,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns _input.Show("Type", "TXT"); _input.Show("Content", $"\"{token}\""); _input.Show("Note", "Some DNS managers add quotes automatically. A single set is needed."); - await _input.Wait("Please press enter after you've created and verified the record"); + await _input.Wait("Please press <Enter> after you've created and verified the record"); // Pre-pre-validate, allowing the manual user to correct mistakes while (true) @@ -62,7 +62,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns _input.Show("Record", recordName); _input.Show("Type", "TXT"); _input.Show("Content", $"\"{token}\""); - _input.Wait("Please press enter after you've deleted the record"); + _input.Wait("Please press <Enter> after you've deleted the record"); return Task.CompletedTask; } } diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs b/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs index 02953cb..6dbde44 100644 --- a/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs +++ b/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs @@ -19,11 +19,5 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http protected override bool IsEmpty(string path) => !_ftpClient.GetFiles(path).Any(); protected override void WriteFile(string path, string content) => _ftpClient.Upload(path, content); - - public override Task CleanUp() - { - base.CleanUp(); - return Task.CompletedTask; - } } } diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs index 0ec872a..4e18b1a 100644 --- a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs +++ b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs @@ -117,7 +117,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins } catch (HttpRequestException hrex) { - _log.Warning("Preliminary validation failed because {hrex}. The ACME server might have a different perspective", hrex.Message); + _log.Warning("Preliminary validation failed because '{hrex}'", hrex.Message); } catch (Exception ex) { diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptions.cs b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptions.cs index 4f53cc4..945a5ef 100644 --- a/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptions.cs +++ b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptions.cs @@ -8,7 +8,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http internal class SelfHostingOptions : ValidationPluginOptions<SelfHosting> { public override string Name => "SelfHosting"; - public override string Description => "Serve verification files from memory (recommended)"; + public override string Description => "Serve verification files from memory"; /// <summary> /// Alternative port for validation. Note that ACME always requires diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs b/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs index 2b7ef0c..c3d5c08 100644 --- a/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs +++ b/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs @@ -1,6 +1,5 @@ using PKISharp.WACS.Clients; using System.Linq; -using System.Threading.Tasks; namespace PKISharp.WACS.Plugins.ValidationPlugins.Http { @@ -19,11 +18,5 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http protected override bool IsEmpty(string path) => !_sshFtpClient.GetFiles(path).Any(); protected override void WriteFile(string path, string content) => _sshFtpClient.Upload(path, content); - - public override Task CleanUp() - { - base.CleanUp(); - return Task.CompletedTask; - } } } diff --git a/src/main.lib/Plugins/ValidationPlugins/Validation.cs b/src/main.lib/Plugins/ValidationPlugins/Validation.cs index f3cfdef..e5e42f4 100644 --- a/src/main.lib/Plugins/ValidationPlugins/Validation.cs +++ b/src/main.lib/Plugins/ValidationPlugins/Validation.cs @@ -53,27 +53,9 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins /// </summary> public abstract Task CleanUp(); + /// <summary> + /// Is the plugin currently disabled + /// </summary> public virtual bool Disabled => false; - - #region IDisposable - - private bool disposedValue = false; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - CleanUp(); - } - disposedValue = true; - } - } - - public void Dispose() => Dispose(true); - - #endregion - } } diff --git a/src/main.lib/RenewalCreator.cs b/src/main.lib/RenewalCreator.cs new file mode 100644 index 0000000..4875cb3 --- /dev/null +++ b/src/main.lib/RenewalCreator.cs @@ -0,0 +1,347 @@ +using Autofac; +using PKISharp.WACS.Configuration; +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Extensions; +using PKISharp.WACS.Plugins.Base.Factories.Null; +using PKISharp.WACS.Plugins.Base.Options; +using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace PKISharp.WACS +{ + internal class RenewalCreator + { + private readonly IInputService _input; + private readonly ILogService _log; + private readonly IRenewalStore _renewalStore; + private readonly MainArguments _args; + private readonly PasswordGenerator _passwordGenerator; + private readonly ISettingsService _settings; + private readonly IContainer _container; + private readonly IAutofacBuilder _scopeBuilder; + private readonly ExceptionHandler _exceptionHandler; + private readonly RenewalExecutor _renewalExecution; + + public RenewalCreator( + PasswordGenerator passwordGenerator, MainArguments args, + IRenewalStore renewalStore, IContainer container, + IInputService input, ILogService log, + ISettingsService settings, IAutofacBuilder autofacBuilder, + ExceptionHandler exceptionHandler, RenewalExecutor renewalExecutor) + { + _passwordGenerator = passwordGenerator; + _renewalStore = renewalStore; + _args = args; + _input = input; + _log = log; + _settings = settings; + _container = container; + _scopeBuilder = autofacBuilder; + _exceptionHandler = exceptionHandler; + _renewalExecution = renewalExecutor; + } + + /// <summary> + /// If renewal is already Scheduled, replace it with the new options + /// </summary> + /// <param name="target"></param> + /// <returns></returns> + private async Task<Renewal> CreateRenewal(Renewal temp, RunLevel runLevel) + { + // First check by id + var existing = _renewalStore.FindByArguments(temp.Id, null).FirstOrDefault(); + + // If Id has been specified, we don't consider the Friendlyname anymore + // So specifying --id becomes a way to create duplicate certificates + // with the same --friendlyname in unattended mode. + if (existing == null && string.IsNullOrEmpty(_args.Id)) + { + existing = _renewalStore.FindByArguments(null, temp.LastFriendlyName).FirstOrDefault(); + } + + // This will be a completely new renewal, no further processing needed + if (existing == null) + { + return temp; + } + + // Match found with existing certificate, determine if we want to overwrite + // it or create it side by side with the current one. + if (runLevel.HasFlag(RunLevel.Interactive)) + { + _input.Show("Existing renewal", existing.ToString(_input), true); + if (!await _input.PromptYesNo($"Overwrite?", true)) + { + return temp; + } + } + + // Move settings from temporary renewal over to + // the pre-existing one that we are overwriting + _log.Warning("Overwriting previously created renewal"); + existing.Updated = true; + existing.TargetPluginOptions = temp.TargetPluginOptions; + existing.CsrPluginOptions = temp.CsrPluginOptions; + existing.StorePluginOptions = temp.StorePluginOptions; + existing.ValidationPluginOptions = temp.ValidationPluginOptions; + existing.InstallationPluginOptions = temp.InstallationPluginOptions; + return existing; + } + + /// <summary> + /// Setup a new scheduled renewal + /// </summary> + /// <param name="runLevel"></param> + internal async Task SetupRenewal(RunLevel runLevel) + { + if (_args.Test) + { + runLevel |= RunLevel.Test; + } + if (_args.Force) + { + runLevel |= RunLevel.IgnoreCache; + } + _log.Information(LogType.All, "Running in mode: {runLevel}", runLevel); + var tempRenewal = Renewal.Create(_args.Id, _settings.ScheduledTask.RenewalDays, _passwordGenerator); + using var configScope = _scopeBuilder.Configuration(_container, tempRenewal, runLevel); + // Choose target plugin + var targetPluginOptionsFactory = configScope.Resolve<ITargetPluginOptionsFactory>(); + if (targetPluginOptionsFactory is INull) + { + _exceptionHandler.HandleException(message: $"No target plugin could be selected"); + return; + } + if (targetPluginOptionsFactory.Disabled) + { + _exceptionHandler.HandleException(message: $"Target plugin {targetPluginOptionsFactory.Name} is not available to the current user, try running as administrator"); + return; + } + var targetPluginOptions = runLevel.HasFlag(RunLevel.Unattended) ? + await targetPluginOptionsFactory.Default() : + await targetPluginOptionsFactory.Aquire(_input, runLevel); + if (targetPluginOptions == null) + { + _exceptionHandler.HandleException(message: $"Target plugin {targetPluginOptionsFactory.Name} aborted or failed"); + return; + } + tempRenewal.TargetPluginOptions = targetPluginOptions; + + // Generate Target and validation plugin choice + Target? initialTarget = null; + IValidationPluginOptionsFactory? validationPluginOptionsFactory = null; + using (var targetScope = _scopeBuilder.Target(_container, tempRenewal, runLevel)) + { + 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); + + // 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)) + { + tempRenewal.FriendlyName = alt; + } + } + 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; + } + } + + // Configure validation + try + { + var validationOptions = runLevel.HasFlag(RunLevel.Unattended) + ? await validationPluginOptionsFactory.Default(initialTarget) + : await validationPluginOptionsFactory.Aquire(initialTarget, _input, runLevel); + if (validationOptions == null) + { + _exceptionHandler.HandleException(message: $"Validation plugin {validationPluginOptionsFactory.Name} was unable to generate options"); + return; + } + tempRenewal.ValidationPluginOptions = validationOptions; + } + catch (Exception ex) + { + _exceptionHandler.HandleException(ex, $"Validation plugin {validationPluginOptionsFactory.Name} aborted or failed"); + return; + } + + // Choose CSR plugin + if (initialTarget.CsrBytes == null) + { + var csrPluginOptionsFactory = configScope.Resolve<ICsrPluginOptionsFactory>(); + if (csrPluginOptionsFactory is INull) + { + _exceptionHandler.HandleException(message: $"No CSR plugin could be selected"); + return; + } + + // Configure CSR + try + { + var csrOptions = runLevel.HasFlag(RunLevel.Unattended) ? + await csrPluginOptionsFactory.Default() : + await csrPluginOptionsFactory.Aquire(_input, runLevel); + if (csrOptions == null) + { + _exceptionHandler.HandleException(message: $"CSR plugin {csrPluginOptionsFactory.Name} was unable to generate options"); + return; + } + tempRenewal.CsrPluginOptions = csrOptions; + } + catch (Exception ex) + { + _exceptionHandler.HandleException(ex, $"CSR plugin {csrPluginOptionsFactory.Name} aborted or failed"); + return; + } + } + + // Choose and configure store plugins + var resolver = configScope.Resolve<IResolver>(); + var storePluginOptionsFactories = new List<IStorePluginOptionsFactory>(); + try + { + while (true) + { + var storePluginOptionsFactory = await resolver.GetStorePlugin(configScope, storePluginOptionsFactories); + if (storePluginOptionsFactory == null) + { + _exceptionHandler.HandleException(message: $"Store could not be selected"); + return; + } + StorePluginOptions? storeOptions; + try + { + storeOptions = runLevel.HasFlag(RunLevel.Unattended) + ? await storePluginOptionsFactory.Default() + : await storePluginOptionsFactory.Aquire(_input, runLevel); + } + catch (Exception ex) + { + _exceptionHandler.HandleException(ex, $"Store plugin {storePluginOptionsFactory.Name} aborted or failed"); + return; + } + if (storeOptions == null) + { + _exceptionHandler.HandleException(message: $"Store plugin {storePluginOptionsFactory.Name} was unable to generate options"); + return; + } + var isNull = storePluginOptionsFactory is NullStoreOptionsFactory; + if (!isNull || storePluginOptionsFactories.Count == 0) + { + tempRenewal.StorePluginOptions.Add(storeOptions); + storePluginOptionsFactories.Add(storePluginOptionsFactory); + } + if (isNull) + { + break; + } + } + } + catch (Exception ex) + { + _exceptionHandler.HandleException(ex, "Invalid selection of store plugins"); + return; + } + + // Choose and configure installation plugins + var installationPluginFactories = new List<IInstallationPluginOptionsFactory>(); + try + { + while (true) + { + var installationPluginOptionsFactory = await resolver.GetInstallationPlugin(configScope, + tempRenewal.StorePluginOptions.Select(x => x.Instance), + installationPluginFactories); + + if (installationPluginOptionsFactory == null) + { + _exceptionHandler.HandleException(message: $"Installation plugin could not be selected"); + return; + } + InstallationPluginOptions installOptions; + try + { + installOptions = runLevel.HasFlag(RunLevel.Unattended) + ? await installationPluginOptionsFactory.Default(initialTarget) + : await installationPluginOptionsFactory.Aquire(initialTarget, _input, runLevel); + } + catch (Exception ex) + { + _exceptionHandler.HandleException(ex, $"Installation plugin {installationPluginOptionsFactory.Name} aborted or failed"); + return; + } + if (installOptions == null) + { + _exceptionHandler.HandleException(message: $"Installation plugin {installationPluginOptionsFactory.Name} was unable to generate options"); + return; + } + var isNull = installationPluginOptionsFactory is NullInstallationOptionsFactory; + if (!isNull || installationPluginFactories.Count == 0) + { + tempRenewal.InstallationPluginOptions.Add(installOptions); + installationPluginFactories.Add(installationPluginOptionsFactory); + } + if (isNull) + { + break; + } + } + } + catch (Exception ex) + { + _exceptionHandler.HandleException(ex, "Invalid selection of installation plugins"); + return; + } + + // Try to run for the first time + var renewal = await CreateRenewal(tempRenewal, runLevel); + retry: + var result = await _renewalExecution.Execute(renewal, runLevel); + if (result == null) + { + _exceptionHandler.HandleException(message: $"Create certificate cancelled"); + } + else if (!result.Success) + { + if (runLevel.HasFlag(RunLevel.Interactive) && + await _input.PromptYesNo("Create certificate failed, retry?", false)) + { + goto retry; + } + _exceptionHandler.HandleException(message: $"Create certificate failed: {result?.ErrorMessage}"); + } + else + { + _renewalStore.Save(renewal, result); + } + } + + } +} diff --git a/src/main.lib/RenewalExecutor.cs b/src/main.lib/RenewalExecutor.cs index 9cbe871..70fa79a 100644 --- a/src/main.lib/RenewalExecutor.cs +++ b/src/main.lib/RenewalExecutor.cs @@ -40,7 +40,7 @@ namespace PKISharp.WACS _container = container; } - public async Task<RenewResult?> Renew(Renewal renewal, RunLevel runLevel) + public async Task<RenewResult?> Execute(Renewal renewal, RunLevel runLevel) { using var ts = _scopeBuilder.Target(_container, renewal, runLevel); using var es = _scopeBuilder.Execution(ts, renewal, runLevel); @@ -408,6 +408,17 @@ namespace PKISharp.WACS _log.Debug("Submitting challenge answer"); challenge = await client.AnswerChallenge(challenge); + try + { + _log.Verbose("Starting post-validation cleanup"); + await validationPlugin.CleanUp(); + _log.Verbose("Post-validation cleanup was succesful"); + } + catch (Exception ex) + { + _log.Warning("An error occured during post-validation cleanup: {ex}", ex.Message); + } + if (challenge.Status != AcmeClient.AuthorizationValid) { if (challenge.Error != null) @@ -422,6 +433,8 @@ namespace PKISharp.WACS _log.Information("Authorization result: {Status}", challenge.Status); return valid; } + + } } catch (Exception ex) diff --git a/src/main.lib/RenewalManager.cs b/src/main.lib/RenewalManager.cs index ead45fd..acd477d 100644 --- a/src/main.lib/RenewalManager.cs +++ b/src/main.lib/RenewalManager.cs @@ -2,13 +2,12 @@ using PKISharp.WACS.Configuration; using PKISharp.WACS.DomainObjects; using PKISharp.WACS.Extensions; -using PKISharp.WACS.Plugins.Base.Factories.Null; -using PKISharp.WACS.Plugins.Base.Options; -using PKISharp.WACS.Plugins.Interfaces; +using PKISharp.WACS.Plugins.TargetPlugins; using PKISharp.WACS.Services; using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; namespace PKISharp.WACS @@ -20,383 +19,320 @@ namespace PKISharp.WACS private readonly IRenewalStore _renewalStore; private readonly IArgumentsService _arguments; private readonly MainArguments _args; - private readonly PasswordGenerator _passwordGenerator; - private readonly ISettingsService _settings; private readonly IContainer _container; private readonly IAutofacBuilder _scopeBuilder; private readonly ExceptionHandler _exceptionHandler; - private readonly RenewalExecutor _renewalExecution; + private readonly RenewalExecutor _renewalExecutor; public RenewalManager( - IArgumentsService arguments, PasswordGenerator passwordGenerator, - MainArguments args, IRenewalStore renewalStore, IContainer container, - IInputService input, ILogService log, ISettingsService settings, + IArgumentsService arguments, MainArguments args, + IRenewalStore renewalStore, IContainer container, + IInputService input, ILogService log, IAutofacBuilder autofacBuilder, ExceptionHandler exceptionHandler, RenewalExecutor renewalExecutor) { - _passwordGenerator = passwordGenerator; _renewalStore = renewalStore; _args = args; _input = input; _log = log; _arguments = arguments; - _settings = settings; _container = container; _scopeBuilder = autofacBuilder; _exceptionHandler = exceptionHandler; - _renewalExecution = renewalExecutor; + _renewalExecutor = renewalExecutor; } /// <summary> - /// If renewal is already Scheduled, replace it with the new options + /// Renewal management mode /// </summary> - /// <param name="target"></param> /// <returns></returns> - private async Task<Renewal> CreateRenewal(Renewal temp, RunLevel runLevel) + internal async Task ManageRenewals() { - // First check by id - var existing = _renewalStore.FindByArguments(temp.Id, null).FirstOrDefault(); - - // If Id has been specified, we don't consider the Friendlyname anymore - // So specifying --id becomes a way to create duplicate certificates - // with the same --friendlyname in unattended mode. - if (existing == null && string.IsNullOrEmpty(_args.Id)) + IEnumerable<Renewal> originalSelection = _renewalStore.Renewals.OrderBy(x => x.LastFriendlyName); + var selectedRenewals = originalSelection; + var quit = false; + do { - existing = _renewalStore.FindByArguments(null, temp.LastFriendlyName).FirstOrDefault(); - } + var all = selectedRenewals.Count() == originalSelection.Count(); + var none = selectedRenewals.Count() == 0; + var totalLabel = originalSelection.Count() != 1 ? "renewals" : "renewal"; + var selectionLabel = + all ? $"*all* renewals" : + none ? "no renewals" : + $"{selectedRenewals.Count()} of {originalSelection.Count()} {totalLabel}"; + var renewalSelectedLabel = selectedRenewals.Count() != 1 ? "renewals" : "renewal"; - // This will be a completely new renewal, no further processing needed - if (existing == null) - { - return temp; - } + _input.Show(null, + "Welcome to the renewal manager. Actions selected in the menu below will " + + "be applied to the following list of renewals. You may filter the list to target " + + "your action at a more specific set of renewals, or sort it to make it easier to " + + "find what you're looking for.", + true); - // Match found with existing certificate, determine if we want to overwrite - // it or create it side by side with the current one. - if (runLevel.HasFlag(RunLevel.Interactive)) - { - _input.Show("Existing renewal", existing.ToString(_input), true); - if (!await _input.PromptYesNo($"Overwrite?", true)) - { - return temp; - } - } + await _input.WritePagedList( + selectedRenewals.Select(x => Choice.Create<Renewal?>(x, + description: x.ToString(_input), + color: x.History.Last().Success ? + x.IsDue() ? + ConsoleColor.DarkYellow : + ConsoleColor.Green : + ConsoleColor.Red))); + + var options = new List<Choice<Func<Task>>>(); + options.Add( + Choice.Create<Func<Task>>( + async () => selectedRenewals = await FilterRenewalsMenu(selectedRenewals), + all ? "Apply filter" : "Apply additional filter", "F", + @disabled: selectedRenewals.Count() < 2, + @default: !(selectedRenewals.Count() < 2))); + options.Add( + Choice.Create<Func<Task>>( + async () => selectedRenewals = await SortRenewalsMenu(selectedRenewals), + "Sort renewals", "S", + @disabled: selectedRenewals.Count() < 2)); + options.Add( + Choice.Create<Func<Task>>( + () => { selectedRenewals = originalSelection; return Task.CompletedTask; }, + "Reset sorting and filtering", "X", + @disabled: all, + @default: originalSelection.Count() > 0 && none)); + options.Add( + Choice.Create<Func<Task>>( + async () => { + foreach (var renewal in selectedRenewals) { + var index = selectedRenewals.ToList().IndexOf(renewal) + 1; + _log.Information("Details for renewal {n}/{m}", index, selectedRenewals.Count()); + await ShowRenewal(renewal); + var cont = false; + if (index != selectedRenewals.Count()) + { + cont = await _input.Wait("Press <Enter> to continue or <Esc> to abort"); + if (!cont) + { + break; + } + } + else + { + await _input.Wait(); + } + + } + }, + $"Show details for {selectionLabel}", "D", + @disabled: none)); + options.Add( + Choice.Create<Func<Task>>( + async () => { + WarnAboutRenewalArguments(); + foreach (var renewal in selectedRenewals) + { + var runLevel = RunLevel.Interactive | RunLevel.ForceRenew; + if (_args.Force) + { + runLevel |= RunLevel.IgnoreCache; + } + await ProcessRenewal(renewal, runLevel); + } + }, + $"Run {selectionLabel}", "R", + @disabled: none)); + options.Add( + Choice.Create<Func<Task>>( + async () => { + var confirm = await _input.PromptYesNo($"Are you sure you want to cancel {selectedRenewals.Count()} currently selected {renewalSelectedLabel}?", false); + if (confirm) + { + foreach (var renewal in selectedRenewals) + { + _renewalStore.Cancel(renewal); + }; + originalSelection = _renewalStore.Renewals.OrderBy(x => x.LastFriendlyName); + selectedRenewals = originalSelection; + } + }, + $"Cancel {selectionLabel}", "C", + @disabled: none)); + options.Add( + Choice.Create<Func<Task>>( + async () => { + 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); + } + }; + } + }, + $"Revoke {selectionLabel}", "V", + @disabled: none)); + options.Add( + Choice.Create<Func<Task>>( + () => { quit = true; return Task.CompletedTask; }, + "Back", "Q", + @default: originalSelection.Count() == 0)); - // Move settings from temporary renewal over to - // the pre-existing one that we are overwriting - _log.Warning("Overwriting previously created renewal"); - existing.Updated = true; - existing.TargetPluginOptions = temp.TargetPluginOptions; - existing.CsrPluginOptions = temp.CsrPluginOptions; - existing.StorePluginOptions = temp.StorePluginOptions; - existing.ValidationPluginOptions = temp.ValidationPluginOptions; - existing.InstallationPluginOptions = temp.InstallationPluginOptions; - return existing; + + _input.Show(null, $"Currently selected {selectedRenewals.Count()} of {originalSelection.Count()} {totalLabel}", true); + var chosen = await _input.ChooseFromMenu("Please choose from the menu", options); + await chosen.Invoke(); + } + while (!quit); } /// <summary> - /// Remove renewal from the list of scheduled items + /// Offer user different ways to sort the renewals /// </summary> - internal async Task CancelRenewal(RunLevel runLevel) + /// <param name="current"></param> + /// <returns></returns> + private async Task<IEnumerable<Renewal>> SortRenewalsMenu(IEnumerable<Renewal> current) { - if (runLevel.HasFlag(RunLevel.Unattended)) + var options = new List<Choice<Func<IEnumerable<Renewal>>>> { - if (!_arguments.HasFilter()) - { - _log.Error("Specify which renewal to cancel using the parameter --id or --friendlyname."); - return; - } - var targets = _renewalStore.FindByArguments( - _arguments.MainArguments.Id, - _arguments.MainArguments.FriendlyName); - if (targets.Count() == 0) - { - _log.Error("No renewals matched."); - return; - } - foreach (var r in targets) - { - _renewalStore.Cancel(r); - } - } - else - { - var renewal = await _input.ChooseOptional( - "Which renewal would you like to cancel?", - _renewalStore.Renewals, - x => Choice.Create<Renewal?>(x), - "Back"); - if (renewal != null) - { - if (await _input.PromptYesNo($"Are you sure you want to cancel the renewal for {renewal}", false)) - { - _renewalStore.Cancel(renewal); - } - } - } + Choice.Create<Func<IEnumerable<Renewal>>>( + () => current.OrderBy(x => x.LastFriendlyName), + "Sort by friendly name", + @default: true), + Choice.Create<Func<IEnumerable<Renewal>>>( + () => current.OrderByDescending(x => x.LastFriendlyName), + "Sort by friendly name (descending)"), + Choice.Create<Func<IEnumerable<Renewal>>>( + () => current.OrderBy(x => x.GetDueDate()), + "Sort by due date"), + Choice.Create<Func<IEnumerable<Renewal>>>( + () => current.OrderByDescending(x => x.GetDueDate()), + "Sort by due date (descending)") + }; + var chosen = await _input.ChooseFromMenu("How would you like to sort the renewals list?", options); + return chosen.Invoke(); } /// <summary> - /// Cancel all renewals + /// Offer user different ways to filter the renewals /// </summary> - internal async Task CancelAllRenewals() + /// <param name="current"></param> + /// <returns></returns> + private async Task<IEnumerable<Renewal>> FilterRenewalsMenu(IEnumerable<Renewal> current) { - var renewals = _renewalStore.Renewals; - await _input.WritePagedList(renewals.Select(x => Choice.Create(x))); - if (await _input.PromptYesNo("Are you sure you want to cancel all of these?", false)) + var options = new List<Choice<Func<Task<IEnumerable<Renewal>>>>> { - _renewalStore.Clear(); - } + Choice.Create<Func<Task<IEnumerable<Renewal>>>>( + () => FilterRenewalsById(current), + "Pick from displayed list", + @default: true), + Choice.Create<Func<Task<IEnumerable<Renewal>>>>( + () => FilterRenewalsByFriendlyName(current), + "Filter by friendly name"), + Choice.Create<Func<Task<IEnumerable<Renewal>>>>( + () => Task.FromResult(current.Where(x => x.IsDue())), + "Keep only due renewals"), + Choice.Create<Func<Task<IEnumerable<Renewal>>>>( + () => Task.FromResult(current.Where(x => !x.IsDue())), + "Remove due renewals"), + Choice.Create<Func<Task<IEnumerable<Renewal>>>>( + () => Task.FromResult(current.Where(x => !x.History.Last().Success)), + "Keep only renewals with errors"), + Choice.Create<Func<Task<IEnumerable<Renewal>>>>( + () => Task.FromResult(current.Where(x => x.History.Last().Success)), + "Remove renewals with errors"), + Choice.Create<Func<Task<IEnumerable<Renewal>>>>( + () => Task.FromResult(current), + "Cancel") + }; + var chosen = await _input.ChooseFromMenu("How would you like to filter?", options); + return await chosen.Invoke(); } /// <summary> - /// Setup a new scheduled renewal + /// Filter specific renewals by list index /// </summary> - /// <param name="runLevel"></param> - internal async Task SetupRenewal(RunLevel runLevel) + /// <param name="current"></param> + /// <returns></returns> + private async Task<IEnumerable<Renewal>> FilterRenewalsById(IEnumerable<Renewal> current) { - if (_args.Test) + var rawInput = await _input.RequestString("Please input the list index of the renewal(s) you'd like to select"); + var parts = rawInput.ParseCsv(); + if (parts == null) { - runLevel |= RunLevel.Test; + return current; } - if (_args.Force) + var ret = new List<Renewal>(); + foreach (var part in parts) { - runLevel |= RunLevel.IgnoreCache; - } - _log.Information(LogType.All, "Running in mode: {runLevel}", runLevel); - var tempRenewal = Renewal.Create(_args.Id, _settings.ScheduledTask.RenewalDays, _passwordGenerator); - using var configScope = _scopeBuilder.Configuration(_container, tempRenewal, runLevel); - // Choose target plugin - var targetPluginOptionsFactory = configScope.Resolve<ITargetPluginOptionsFactory>(); - if (targetPluginOptionsFactory is INull) - { - _exceptionHandler.HandleException(message: $"No target plugin could be selected"); - return; - } - if (targetPluginOptionsFactory.Disabled) - { - _exceptionHandler.HandleException(message: $"Target plugin {targetPluginOptionsFactory.Name} is not available to the current user, try running as administrator"); - return; - } - var targetPluginOptions = runLevel.HasFlag(RunLevel.Unattended) ? - await targetPluginOptionsFactory.Default() : - await targetPluginOptionsFactory.Aquire(_input, runLevel); - if (targetPluginOptions == null) - { - _exceptionHandler.HandleException(message: $"Target plugin {targetPluginOptionsFactory.Name} aborted or failed"); - return; - } - tempRenewal.TargetPluginOptions = targetPluginOptions; - - // Generate Target and validation plugin choice - Target? initialTarget = null; - IValidationPluginOptionsFactory? validationPluginOptionsFactory = null; - using (var targetScope = _scopeBuilder.Target(_container, tempRenewal, runLevel)) - { - 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); - - // Choose FriendlyName - if (runLevel.HasFlag(RunLevel.Advanced) && - runLevel.HasFlag(RunLevel.Interactive) && - string.IsNullOrEmpty(_args.FriendlyName)) + if (int.TryParse(part, out var index)) { - var alt = await _input.RequestString($"Suggested friendly name '{initialTarget.FriendlyName}', press <ENTER> to accept or type an alternative"); - if (!string.IsNullOrEmpty(alt)) + if (index > 0 && index <= current.Count()) { - tempRenewal.FriendlyName = alt; - } - } - tempRenewal.LastFriendlyName = initialTarget.FriendlyName; - - // Choose validation plugin - validationPluginOptionsFactory = targetScope.Resolve<IValidationPluginOptionsFactory>(); - if (validationPluginOptionsFactory is INull) - { - _exceptionHandler.HandleException(message: $"No validation plugin could be selected"); - return; - } - } - - // Configure validation - try - { - var validationOptions = runLevel.HasFlag(RunLevel.Unattended) - ? await validationPluginOptionsFactory.Default(initialTarget) - : await validationPluginOptionsFactory.Aquire(initialTarget, _input, runLevel); - if (validationOptions == null) - { - _exceptionHandler.HandleException(message: $"Validation plugin {validationPluginOptionsFactory.Name} was unable to generate options"); - return; - } - tempRenewal.ValidationPluginOptions = validationOptions; - } - catch (Exception ex) - { - _exceptionHandler.HandleException(ex, $"Validation plugin {validationPluginOptionsFactory.Name} aborted or failed"); - return; - } - - // Choose CSR plugin - if (initialTarget.CsrBytes == null) - { - var csrPluginOptionsFactory = configScope.Resolve<ICsrPluginOptionsFactory>(); - if (csrPluginOptionsFactory is INull) - { - _exceptionHandler.HandleException(message: $"No CSR plugin could be selected"); - return; - } - - // Configure CSR - try - { - var csrOptions = runLevel.HasFlag(RunLevel.Unattended) ? - await csrPluginOptionsFactory.Default() : - await csrPluginOptionsFactory.Aquire(_input, runLevel); - if (csrOptions == null) + ret.Add(current.ElementAt(index - 1)); + } + else { - _exceptionHandler.HandleException(message: $"CSR plugin {csrPluginOptionsFactory.Name} was unable to generate options"); - return; + _log.Warning("Input out of range: {part}", part); } - tempRenewal.CsrPluginOptions = csrOptions; - } - catch (Exception ex) + } + else { - _exceptionHandler.HandleException(ex, $"CSR plugin {csrPluginOptionsFactory.Name} aborted or failed"); - return; + _log.Warning("Invalid input: {part}", part); } } + return ret; + } - // Choose and configure store plugins - var resolver = configScope.Resolve<IResolver>(); - var storePluginOptionsFactories = new List<IStorePluginOptionsFactory>(); - try - { - while (true) - { - var storePluginOptionsFactory = await resolver.GetStorePlugin(configScope, storePluginOptionsFactories); - if (storePluginOptionsFactory == null) - { - _exceptionHandler.HandleException(message: $"Store could not be selected"); - return; - } - if (storePluginOptionsFactory is NullStoreOptionsFactory) - { - if (storePluginOptionsFactories.Count == 0) - { - throw new Exception(); - } - break; - } - StorePluginOptions? storeOptions; - try - { - storeOptions = runLevel.HasFlag(RunLevel.Unattended) - ? await storePluginOptionsFactory.Default() - : await storePluginOptionsFactory.Aquire(_input, runLevel); - } - catch (Exception ex) - { - _exceptionHandler.HandleException(ex, $"Store plugin {storePluginOptionsFactory.Name} aborted or failed"); - return; - } - if (storeOptions == null) - { - _exceptionHandler.HandleException(message: $"Store plugin {storePluginOptionsFactory.Name} was unable to generate options"); - return; - } - tempRenewal.StorePluginOptions.Add(storeOptions); - storePluginOptionsFactories.Add(storePluginOptionsFactory); - } - } - catch (Exception ex) - { - _exceptionHandler.HandleException(ex, "Invalid selection of store plugins"); - return; - } - - // Choose and configure installation plugins - var installationPluginFactories = new List<IInstallationPluginOptionsFactory>(); - try + /// <summary> + /// Filter specific renewals by friendly name + /// </summary> + /// <param name="current"></param> + /// <returns></returns> + private async Task<IEnumerable<Renewal>> FilterRenewalsByFriendlyName(IEnumerable<Renewal> current) + { + _input.Show(null, "Please input friendly name to filter renewals by. " + IISArgumentsProvider.PatternExamples, true); + var rawInput = await _input.RequestString("Friendly name"); + var ret = new List<Renewal>(); + var regex = new Regex(rawInput.PatternToRegex()); + foreach (var r in current) { - while (true) + if (regex.Match(r.LastFriendlyName).Success) { - var installationPluginFactory = await resolver.GetInstallationPlugin(configScope, - tempRenewal.StorePluginOptions.Select(x => x.Instance), - installationPluginFactories); - - if (installationPluginFactory == null) - { - _exceptionHandler.HandleException(message: $"Installation plugin could not be selected"); - return; - } - InstallationPluginOptions installOptions; - try - { - installOptions = runLevel.HasFlag(RunLevel.Unattended) - ? await installationPluginFactory.Default(initialTarget) - : await installationPluginFactory.Aquire(initialTarget, _input, runLevel); - } - catch (Exception ex) - { - _exceptionHandler.HandleException(ex, $"Installation plugin {installationPluginFactory.Name} aborted or failed"); - return; - } - if (installOptions == null) - { - _exceptionHandler.HandleException(message: $"Installation plugin {installationPluginFactory.Name} was unable to generate options"); - return; - } - if (installationPluginFactory is NullInstallationOptionsFactory) - { - if (installationPluginFactories.Count == 0) - { - tempRenewal.InstallationPluginOptions.Add(installOptions); - installationPluginFactories.Add(installationPluginFactory); - } - break; - } - tempRenewal.InstallationPluginOptions.Add(installOptions); - installationPluginFactories.Add(installationPluginFactory); + ret.Add(r); } } - catch (Exception ex) - { - _exceptionHandler.HandleException(ex, "Invalid selection of installation plugins"); - return; - } + return ret; + } - // Try to run for the first time - var renewal = await CreateRenewal(tempRenewal, runLevel); - retry: - var result = await _renewalExecution.Renew(renewal, runLevel); - if (result == null) - { - _exceptionHandler.HandleException(message: $"Create certificate cancelled"); - } - else if (!result.Success) + /// <summary> + /// Filters for unattended mode + /// </summary> + /// <param name="command"></param> + /// <returns></returns> + private async Task<IEnumerable<Renewal>> FilterRenewalsByCommandLine(string command) + { + if (_arguments.HasFilter()) { - if (runLevel.HasFlag(RunLevel.Interactive) && - await _input.PromptYesNo("Create certificate failed, retry?", false)) + var targets = _renewalStore.FindByArguments( + _arguments.MainArguments.Id, + _arguments.MainArguments.FriendlyName); + if (targets.Count() == 0) { - goto retry; + _log.Error("No renewals matched."); } - _exceptionHandler.HandleException(message: $"Create certificate failed: {result?.ErrorMessage}"); + return targets; } else { - _renewalStore.Save(renewal, result); + _log.Error($"Specify which renewal to {command} using the parameter --id or --friendlyname."); } + return new List<Renewal>(); } /// <summary> @@ -443,7 +379,7 @@ namespace PKISharp.WACS var notification = _container.Resolve<NotificationService>(); try { - var result = await _renewalExecution.Renew(renewal, runLevel); + var result = await _renewalExecutor.Execute(renewal, runLevel); if (result != null) { _renewalStore.Save(renewal, result); @@ -464,6 +400,11 @@ namespace PKISharp.WACS } } + /// <summary> + /// Show a warning when the user appears to be trying to + /// use command line arguments in combination with a renew + /// command. + /// </summary> internal void WarnAboutRenewalArguments() { if (_arguments.Active) @@ -474,79 +415,98 @@ namespace PKISharp.WACS } } - /// <summary> /// Show certificate details /// </summary> - internal async Task ShowRenewals() + private async Task ShowRenewal(Renewal renewal) { - var renewal = await _input.ChooseOptional( - "Type the number of a renewal to show its details, or press enter to go back", - _renewalStore.Renewals, - x => Choice.Create<Renewal?>(x, + try + { + _input.Show("Id", renewal.Id, true); + _input.Show("File", $"{renewal.Id}.renewal.json"); + _input.Show("FriendlyName", string.IsNullOrEmpty(renewal.FriendlyName) ? $"[Auto] {renewal.LastFriendlyName}" : renewal.FriendlyName); + _input.Show(".pfx password", renewal.PfxPassword?.Value); + _input.Show("Renewal due", renewal.GetDueDate()?.ToString() ?? "now"); + _input.Show("Renewed", $"{renewal.History.Where(x => x.Success).Count()} times"); + renewal.TargetPluginOptions.Show(_input); + renewal.ValidationPluginOptions.Show(_input); + if (renewal.CsrPluginOptions != null) + { + renewal.CsrPluginOptions.Show(_input); + } + foreach (var ipo in renewal.StorePluginOptions) + { + ipo.Show(_input); + } + foreach (var ipo in renewal.InstallationPluginOptions) + { + ipo.Show(_input); + } + _input.Show("History"); + await _input.WritePagedList(renewal.History.Select(x => Choice.Create(x))); + } + catch (Exception ex) + { + _log.Error(ex, "Unable to list details for target"); + } + } + + #region Unattended + + /// <summary> + /// For command line --list + /// </summary> + /// <returns></returns> + internal async Task ShowRenewalsUnattended() + { + await _input.WritePagedList( + _renewalStore.Renewals.Select(x => Choice.Create<Renewal?>(x, description: x.ToString(_input), color: x.History.Last().Success ? x.IsDue() ? ConsoleColor.DarkYellow : ConsoleColor.Green : - ConsoleColor.Red), - "Back"); + ConsoleColor.Red))); + } - if (renewal != null) + /// <summary> + /// Cancel certificate from the command line + /// </summary> + /// <returns></returns> + internal async Task CancelRenewalsUnattended() + { + var targets = await FilterRenewalsByCommandLine("cancel"); + foreach (var t in targets) { - try - { - _input.Show("Renewal"); - _input.Show("Id", renewal.Id); - _input.Show("File", $"{renewal.Id}.renewal.json"); - _input.Show("FriendlyName", string.IsNullOrEmpty(renewal.FriendlyName) ? $"[Auto] {renewal.LastFriendlyName}" : renewal.FriendlyName); - _input.Show(".pfx password", renewal.PfxPassword?.Value); - _input.Show("Renewal due", renewal.GetDueDate()?.ToString() ?? "now"); - _input.Show("Renewed", $"{renewal.History.Where(x => x.Success).Count()} times"); - renewal.TargetPluginOptions.Show(_input); - renewal.ValidationPluginOptions.Show(_input); - if (renewal.CsrPluginOptions != null) - { - renewal.CsrPluginOptions.Show(_input); - } - foreach (var ipo in renewal.StorePluginOptions) - { - ipo.Show(_input); - } - foreach (var ipo in renewal.InstallationPluginOptions) - { - ipo.Show(_input); - } - _input.Show("History"); - await _input.WritePagedList(renewal.History.Select(x => Choice.Create(x))); - } - catch (Exception ex) - { - _log.Error(ex, "Unable to list details for target"); - } + _renewalStore.Cancel(t); } } /// <summary> - /// Renew specific certificate + /// Revoke certifcate from the command line /// </summary> - internal async Task RenewSpecific() + /// <returns></returns> + internal async Task RevokeCertificatesUnattended() { - var renewal = await _input.ChooseOptional( - "Which renewal would you like to run?", - _renewalStore.Renewals, - x => Choice.Create<Renewal?>(x), - "Back"); - if (renewal != null) + _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"); + foreach (var renewal in renewals) { - var runLevel = RunLevel.Interactive | RunLevel.ForceRenew; - if (_args.Force) + using var scope = _scopeBuilder.Execution(_container, renewal, RunLevel.Unattended); + var cs = scope.Resolve<ICertificateService>(); + try { - runLevel |= RunLevel.IgnoreCache; + await cs.RevokeCertificate(renewal); + renewal.History.Add(new RenewResult("Certificate revoked")); + } + catch (Exception ex) + { + _exceptionHandler.HandleException(ex); } - WarnAboutRenewalArguments(); - await ProcessRenewal(renewal, runLevel); } } + + #endregion + } }
\ No newline at end of file diff --git a/src/main.lib/Services/AutofacBuilder.cs b/src/main.lib/Services/AutofacBuilder.cs index 84ba44a..ee92def 100644 --- a/src/main.lib/Services/AutofacBuilder.cs +++ b/src/main.lib/Services/AutofacBuilder.cs @@ -30,7 +30,7 @@ namespace PKISharp.WACS.Services { var realSettings = main.Resolve<ISettingsService>(); var realArguments = main.Resolve<IArgumentsService>(); - + builder.Register(c => new MainArguments { BaseUri = fromUri.ToString() }). @@ -52,9 +52,7 @@ namespace PKISharp.WACS.Services WithParameter(new TypedParameter(typeof(ISettingsService), realSettings)). SingleInstance(); - builder.RegisterType<RenewalStoreDisk>(). - WithParameter(new TypedParameter(typeof(IArgumentsService), realArguments)). - WithParameter(new TypedParameter(typeof(ISettingsService), realSettings)). + builder.Register((scope) => main.Resolve<IRenewalStore>()). As<IRenewalStore>(). SingleInstance(); diff --git a/src/main.lib/Services/CertificateService.cs b/src/main.lib/Services/CertificateService.cs index 12990dd..d30e601 100644 --- a/src/main.lib/Services/CertificateService.cs +++ b/src/main.lib/Services/CertificateService.cs @@ -90,8 +90,15 @@ namespace PKISharp.WACS.Services { foreach (var f in _cache.GetFiles($"{prefix}{renewal.Id}{postfix}")) { - _log.Verbose("Deleting {file} from certificate cache @ {folder}", f.Name, _cache.FullName); - f.Delete(); + _log.Verbose("Deleting {file} from {folder}", f.Name, _cache.FullName); + try + { + f.Delete(); + } + catch (Exception ex) + { + _log.Warning("Error deleting {file} from {folder}: {message}", f.Name, _cache.FullName, ex.Message); + } } } void ICertificateService.Delete(Renewal renewal) => ClearCache(renewal); @@ -281,7 +288,7 @@ namespace PKISharp.WACS.Services if (runLevel.HasFlag(RunLevel.IgnoreCache)) { _log.Warning("Cached certificate available but not used with the --{switch} switch. " + - "Use 'Renew specific' or 'Renew all' in the main menu to run unscheduled " + + "Use 'Manage renewals > Run renewal' in the main menu to run unscheduled " + "renewals without hitting rate limits.", nameof(MainArguments.Force).ToLower()); } diff --git a/src/main.lib/Services/DomainParseService.cs b/src/main.lib/Services/DomainParseService.cs index 85e21ca..a74dfca 100644 --- a/src/main.lib/Services/DomainParseService.cs +++ b/src/main.lib/Services/DomainParseService.cs @@ -55,7 +55,6 @@ namespace PKISharp.WACS.Services public string GetTLD(string fulldomain) => Parser.Get(fulldomain).TLD; public string GetDomain(string fulldomain) => Parser.Get(fulldomain).Domain; - public string GetSubDomain(string fulldomain) => Parser.Get(fulldomain).SubDomain; /// <summary> /// Regular 7 day file cache in the configuration folder diff --git a/src/main.lib/Services/InputService.cs b/src/main.lib/Services/InputService.cs index ef6d4fc..bc5204c 100644 --- a/src/main.lib/Services/InputService.cs +++ b/src/main.lib/Services/InputService.cs @@ -44,7 +44,7 @@ namespace PKISharp.WACS.Services } } - public Task<bool> Wait(string message = "Press enter to continue...") + public Task<bool> Wait(string message = "Press <Enter> to continue...") { Validate(message); CreateSpace(); diff --git a/src/main.lib/Services/Interfaces/IInputService.cs b/src/main.lib/Services/Interfaces/IInputService.cs index 9ede4e3..48daab4 100644 --- a/src/main.lib/Services/Interfaces/IInputService.cs +++ b/src/main.lib/Services/Interfaces/IInputService.cs @@ -14,7 +14,7 @@ namespace PKISharp.WACS.Services Task<string> RequestString(string what); Task<string> RequestString(string[] what); void Show(string? label, string? value = null, bool first = false, int level = 0); - Task<bool> Wait(string message = ""); + Task<bool> Wait(string message = "Press <Enter> to continue"); Task WritePagedList(IEnumerable<Choice> listItems); string FormatDate(DateTime date); } diff --git a/src/main.lib/Services/Legacy/Importer.cs b/src/main.lib/Services/Legacy/Importer.cs index 766e55e..4359d3c 100644 --- a/src/main.lib/Services/Legacy/Importer.cs +++ b/src/main.lib/Services/Legacy/Importer.cs @@ -1,4 +1,5 @@ -using PKISharp.WACS.Configuration; +using PKISharp.WACS.Clients.Acme; +using PKISharp.WACS.Configuration; using PKISharp.WACS.DomainObjects; using PKISharp.WACS.Extensions; using PKISharp.WACS.Plugins.Base.Factories.Null; @@ -22,36 +23,68 @@ namespace PKISharp.WACS.Services.Legacy private readonly IRenewalStore _currentRenewal; private readonly ILogService _log; private readonly ISettingsService _settings; + private readonly IInputService _input; private readonly TaskSchedulerService _currentTaskScheduler; private readonly LegacyTaskSchedulerService _legacyTaskScheduler; private readonly PasswordGenerator _passwordGenerator; + private readonly AcmeClient _acmeClient; - public Importer(ILogService log, ILegacyRenewalService legacyRenewal, + public Importer( + ILogService log, ILegacyRenewalService legacyRenewal, ISettingsService settings, IRenewalStore currentRenewal, + IInputService input, LegacyTaskSchedulerService legacyTaskScheduler, TaskSchedulerService currentTaskScheduler, - PasswordGenerator passwordGenerator) + PasswordGenerator passwordGenerator, + AcmeClient acmeClient) { _legacyRenewal = legacyRenewal; _currentRenewal = currentRenewal; _log = log; _settings = settings; + _input = input; _currentTaskScheduler = currentTaskScheduler; _legacyTaskScheduler = legacyTaskScheduler; _passwordGenerator = passwordGenerator; + _acmeClient = acmeClient; } public async Task Import(RunLevel runLevel) { + + if (!_legacyRenewal.Renewals.Any()) + { + _log.Warning("No legacy renewals found"); + } _log.Information("Legacy renewals {x}", _legacyRenewal.Renewals.Count().ToString()); _log.Information("Current renewals {x}", _currentRenewal.Renewals.Count().ToString()); + _log.Information("Step {x}/3: convert renewals", 1); foreach (var legacyRenewal in _legacyRenewal.Renewals) { var converted = Convert(legacyRenewal); _currentRenewal.Import(converted); } + _log.Information("Step {x}/3: create new scheduled task", 2); await _currentTaskScheduler.EnsureTaskScheduler(runLevel | RunLevel.Import, true); _legacyTaskScheduler.StopTaskScheduler(); + + _log.Information("Step {x}/3: ensure ACMEv2 account", 3); + await _acmeClient.GetAccount(); + var listCommand = "--list"; + var renewCommand = "--renew"; + if (runLevel.HasFlag(RunLevel.Interactive)) + { + listCommand = "Manage renewals"; + renewCommand = "Run"; + } + _input.Show(null, + value: $"The renewals have now been imported into this new version " + + "of the program. Nothing else will happen until new scheduled task is " + + "first run *or* you trigger them manually. It is highly recommended " + + $"to review the imported items with '{listCommand}' and to monitor the " + + $"results of the first execution with '{renewCommand}'.", + @first: true); + } public Renewal Convert(LegacyScheduledRenewal legacy) diff --git a/src/main.lib/Services/LogService.cs b/src/main.lib/Services/LogService.cs index c629a29..3b6fde5 100644 --- a/src/main.lib/Services/LogService.cs +++ b/src/main.lib/Services/LogService.cs @@ -6,6 +6,7 @@ using Serilog.Sinks.SystemConsole.Themes; using System; using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; namespace PKISharp.WACS.Services { @@ -32,17 +33,27 @@ namespace PKISharp.WACS.Services _levelSwitch = new LoggingLevelSwitch(initialMinimumLevel: initialLevel); try { + var theme = + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + Environment.OSVersion.Version.Major == 10 ? + (ConsoleTheme)AnsiConsoleTheme.Code : + SystemConsoleTheme.Literate; + _screenLogger = new LoggerConfiguration() .MinimumLevel.ControlledBy(_levelSwitch) .Enrich.FromLogContext() .Filter.ByIncludingOnly(x => { Dirty = true; return true; }) - .WriteTo.Console(outputTemplate: " {Message:l}{NewLine}", theme: AnsiConsoleTheme.Code) + .WriteTo.Console( + outputTemplate: " {Message:l}{NewLine}", + theme: theme) .CreateLogger(); _debugScreenLogger = new LoggerConfiguration() .MinimumLevel.ControlledBy(_levelSwitch) .Enrich.FromLogContext() .Filter.ByIncludingOnly(x => { Dirty = true; return true; }) - .WriteTo.Console(outputTemplate: " [{Level:u4}] {Message:l}{NewLine}{Exception}", theme: AnsiConsoleTheme.Code) + .WriteTo.Console( + outputTemplate: " [{Level:u4}] {Message:l}{NewLine}{Exception}", + theme: theme) .CreateLogger(); } catch (Exception ex) @@ -121,9 +132,9 @@ namespace PKISharp.WACS.Services Verbose("Verbose mode logging enabled"); } - public void Verbose(string message, params object?[] items) => Verbose(LogType.Screen, message, items); + public void Verbose(string message, params object?[] items) => Verbose(LogType.Screen | LogType.Disk, message, items); - public void Debug(string message, params object?[] items) => Debug(LogType.Screen, message, items); + public void Debug(string message, params object?[] items) => Debug(LogType.Screen | LogType.Disk, message, items); public void Warning(string message, params object?[] items) => Warning(LogType.All, message, items); @@ -131,7 +142,7 @@ namespace PKISharp.WACS.Services public void Error(Exception ex, string message, params object?[] items) => Error(LogType.All, ex, message, items); - public void Information(string message, params object?[] items) => Information(LogType.Screen, message, items); + public void Information(string message, params object?[] items) => Information(LogType.Screen | LogType.Disk, message, items); public void Information(LogType logType, string message, params object?[] items) => _Information(logType, message, items); diff --git a/src/main.lib/Services/PluginService.cs b/src/main.lib/Services/PluginService.cs index e0763b7..2f21930 100644 --- a/src/main.lib/Services/PluginService.cs +++ b/src/main.lib/Services/PluginService.cs @@ -97,7 +97,7 @@ namespace PKISharp.WACS.Services _targetOptionFactories = GetResolvable<ITargetPluginOptionsFactory>(); _validationOptionFactories = GetResolvable<IValidationPluginOptionsFactory>(); _csrOptionFactories = GetResolvable<ICsrPluginOptionsFactory>(); - _storeOptionFactories = GetResolvable<IStorePluginOptionsFactory>(); + _storeOptionFactories = GetResolvable<IStorePluginOptionsFactory>(true); _installationOptionFactories = GetResolvable<IInstallationPluginOptionsFactory>(true); _target = GetResolvable<ITargetPlugin>(); diff --git a/src/main.lib/Services/RenewalStore.cs b/src/main.lib/Services/RenewalStore.cs index 0285774..a061417 100644 --- a/src/main.lib/Services/RenewalStore.cs +++ b/src/main.lib/Services/RenewalStore.cs @@ -1,7 +1,9 @@ using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Extensions; using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; namespace PKISharp.WACS.Services { @@ -41,7 +43,8 @@ namespace PKISharp.WACS.Services var ret = Renewals; if (!string.IsNullOrEmpty(friendlyName)) { - ret = ret.Where(x => string.Equals(friendlyName, x.LastFriendlyName, StringComparison.CurrentCultureIgnoreCase)); + var regex = new Regex(friendlyName.ToLower().PatternToRegex()); + ret = ret.Where(x => regex.IsMatch(x.LastFriendlyName?.ToLower())); } if (!string.IsNullOrEmpty(id)) { diff --git a/src/main.lib/Wacs.cs b/src/main.lib/Wacs.cs index 778872e..9c0bc43 100644 --- a/src/main.lib/Wacs.cs +++ b/src/main.lib/Wacs.cs @@ -3,12 +3,12 @@ using PKISharp.WACS.Clients; using PKISharp.WACS.Clients.Acme; using PKISharp.WACS.Clients.IIS; using PKISharp.WACS.Configuration; -using PKISharp.WACS.DomainObjects; using PKISharp.WACS.Extensions; using PKISharp.WACS.Services; using PKISharp.WACS.Services.Legacy; using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -24,6 +24,7 @@ namespace PKISharp.WACS.Host private readonly ILifetimeScope _container; private readonly MainArguments _args; private readonly RenewalManager _renewalManager; + private readonly RenewalCreator _renewalCreator; private readonly IAutofacBuilder _scopeBuilder; private readonly ExceptionHandler _exceptionHandler; private readonly UserRoleService _userRoleService; @@ -50,8 +51,6 @@ namespace PKISharp.WACS.Host _log.Warning("Error setting text encoding to {name}", _settings.UI.TextEncoding); } - ShowBanner(); - _arguments = _container.Resolve<IArgumentsService>(); _arguments.ShowCommandLine(); _args = _arguments.MainArguments; @@ -63,6 +62,9 @@ namespace PKISharp.WACS.Host _renewalManager = container.Resolve<RenewalManager>( new TypedParameter(typeof(IContainer), _container), new TypedParameter(typeof(RenewalExecutor), renewalExecutor)); + _renewalCreator = container.Resolve<RenewalCreator>( + new TypedParameter(typeof(IContainer), _container), + new TypedParameter(typeof(RenewalExecutor), renewalExecutor)); } /// <summary> @@ -70,6 +72,9 @@ namespace PKISharp.WACS.Host /// </summary> public async Task<int> Start() { + // Show informational message and start-up diagnostics + await ShowBanner(); + // Version display (handled by ShowBanner in constructor) if (_args.Version) { @@ -103,12 +108,17 @@ namespace PKISharp.WACS.Host } else if (_args.List) { - await _renewalManager.ShowRenewals(); + await _renewalManager.ShowRenewalsUnattended(); await CloseDefault(); } else if (_args.Cancel) { - await _renewalManager.CancelRenewal(RunLevel.Unattended); + await _renewalManager.CancelRenewalsUnattended(); + await CloseDefault(); + } + else if (_args.Revoke) + { + await _renewalManager.RevokeCertificatesUnattended(); await CloseDefault(); } else if (_args.Renew) @@ -123,7 +133,7 @@ namespace PKISharp.WACS.Host } else if (!string.IsNullOrEmpty(_args.Target)) { - await _renewalManager.SetupRenewal(RunLevel.Unattended); + await _renewalCreator.SetupRenewal(RunLevel.Unattended); await CloseDefault(); } else if (_args.Encrypt) @@ -158,7 +168,7 @@ namespace PKISharp.WACS.Host /// <summary> /// Show banner /// </summary> - private void ShowBanner() + private async Task ShowBanner() { var build = ""; #if DEBUG @@ -180,6 +190,8 @@ namespace PKISharp.WACS.Host if (_args != null) { _log.Information("ACME server {ACME}", _settings.BaseUri); + var client = _container.Resolve<AcmeClient>(); + await client.CheckNetwork(); } if (iis.Major > 0) { @@ -220,29 +232,31 @@ namespace PKISharp.WACS.Host /// </summary> private async Task MainMenu() { + var total = _renewalStore.Renewals.Count(); + var due = _renewalStore.Renewals.Count(x => x.IsDue()); + var error = _renewalStore.Renewals.Count(x => !x.History.Last().Success); + var options = new List<Choice<Func<Task>>> { Choice.Create<Func<Task>>( - () => _renewalManager.SetupRenewal(RunLevel.Interactive | RunLevel.Simple), + () => _renewalCreator.SetupRenewal(RunLevel.Interactive | RunLevel.Simple), "Create new certificate (simple for IIS)", "N", @default: _userRoleService.AllowIIS, disabled: !_userRoleService.AllowIIS), Choice.Create<Func<Task>>( - () => _renewalManager.SetupRenewal(RunLevel.Interactive | RunLevel.Advanced), + () => _renewalCreator.SetupRenewal(RunLevel.Interactive | RunLevel.Advanced), "Create new certificate (full options)", "M", @default: !_userRoleService.AllowIIS), Choice.Create<Func<Task>>( - () => _renewalManager.ShowRenewals(), - "List scheduled renewals", "L"), - Choice.Create<Func<Task>>( - () => _renewalManager.CheckRenewals(RunLevel.Interactive), - "Renew scheduled", "R"), - Choice.Create<Func<Task>>( - () => _renewalManager.RenewSpecific(), - "Renew specific", "S"), + () => _renewalManager.CheckRenewals(RunLevel.Interactive), + $"Run scheduled renewals ({due} currently due)", "R", + color: due == 0 ? (ConsoleColor?)null : ConsoleColor.Yellow, + disabled: due == 0), Choice.Create<Func<Task>>( - () => _renewalManager.CheckRenewals(RunLevel.Interactive | RunLevel.ForceRenew), - "Renew *all*", "A"), + () => _renewalManager.ManageRenewals(), + $"Manage renewals ({total} total{(error == 0 ? "" : $", {error} in error")})", "A", + color: error == 0 ? (ConsoleColor?)null : ConsoleColor.Red, + disabled: total == 0), Choice.Create<Func<Task>>( () => ExtraMenu(), "More options...", "O"), @@ -262,15 +276,6 @@ namespace PKISharp.WACS.Host var options = new List<Choice<Func<Task>>> { Choice.Create<Func<Task>>( - () => _renewalManager.CancelRenewal(RunLevel.Interactive), - "Cancel scheduled renewal", "C"), - Choice.Create<Func<Task>>( - () => _renewalManager.CancelAllRenewals(), - "Cancel *all* scheduled renewals", "X"), - Choice.Create<Func<Task>>( - () => RevokeCertificate(), - "Revoke certificate", "V"), - Choice.Create<Func<Task>>( () => _taskScheduler.EnsureTaskScheduler(RunLevel.Interactive | RunLevel.Advanced, true), "(Re)create scheduled task", "T", disabled: !_userRoleService.AllowTaskScheduler), @@ -297,35 +302,6 @@ namespace PKISharp.WACS.Host } /// <summary> - /// Revoke certificate - /// </summary> - private async Task RevokeCertificate() - { - var renewal = await _input.ChooseOptional( - "Which certificate would you like to revoke?", - _renewalStore.Renewals, - x => Choice.Create<Renewal?>(x), - "Back"); - if (renewal != null) - { - if (await _input.PromptYesNo($"Are you sure you want to revoke {renewal}? This should only be done in case of a security breach.", false)) - { - using var scope = _scopeBuilder.Execution(_container, renewal, RunLevel.Unattended); - var cs = scope.Resolve<ICertificateService>(); - try - { - await cs.RevokeCertificate(renewal); - renewal.History.Add(new RenewResult("Certificate revoked")); - } - catch (Exception ex) - { - _exceptionHandler.HandleException(ex); - } - } - } - } - - /// <summary> /// Load renewals from 1.9.x /// </summary> private async Task Import(RunLevel runLevel) @@ -402,7 +378,7 @@ namespace PKISharp.WACS.Host { throw new InvalidOperationException(); } - _input.Show("Account ID", acmeAccount.Payload.Id, true); + _input.Show("Account ID", acmeAccount.Payload.Id ?? "-", true); _input.Show("Created", acmeAccount.Payload.CreatedAt); _input.Show("Initial IP", acmeAccount.Payload.InitialIp); _input.Show("Status", acmeAccount.Payload.Status); diff --git a/src/main.test/Mock/Services/CertificateService.cs b/src/main.test/Mock/Services/CertificateService.cs index 4cded26..8325620 100644 --- a/src/main.test/Mock/Services/CertificateService.cs +++ b/src/main.test/Mock/Services/CertificateService.cs @@ -12,7 +12,7 @@ namespace PKISharp.WACS.UnitTests.Mock.Services internal class CertificateService : ICertificateService { public CertificateInfo CachedInfo(Renewal renewal, Target target = null) => null; - public void Delete(Renewal renewal) => throw new NotImplementedException(); + public void Delete(Renewal renewal) {} public void Encrypt() { } diff --git a/src/main.test/Tests/DnsValidationTests/When_resolving_name_servers.cs b/src/main.test/Tests/DnsValidationTests/When_resolving_name_servers.cs index a9608c2..f72d970 100644 --- a/src/main.test/Tests/DnsValidationTests/When_resolving_name_servers.cs +++ b/src/main.test/Tests/DnsValidationTests/When_resolving_name_servers.cs @@ -39,5 +39,15 @@ namespace PKISharp.WACS.UnitTests.Tests.DnsValidationTests [DataRow("activesync.dynu.net")] [DataRow("tweakers.net")] public void Should_find_nameserver(string domain) => _ = _dnsClient.GetClients(domain).Result; + + + [TestMethod] + [DataRow("_acme-challenge.acmedns.wouter.tinus.online")] + public void Should_Find_Txt(string domain) + { + var client = _dnsClient.GetClients(domain).Result.First(); + var tokens = client.GetTextRecordValues(domain, 0).Result; + Assert.IsTrue(tokens.Any()); + } } } diff --git a/src/main.test/Tests/InstallationPluginTests/MultipleInstallerTests.cs b/src/main.test/Tests/InstallationPluginTests/MultipleInstallerTests.cs index 80a43e8..209794d 100644 --- a/src/main.test/Tests/InstallationPluginTests/MultipleInstallerTests.cs +++ b/src/main.test/Tests/InstallationPluginTests/MultipleInstallerTests.cs @@ -27,6 +27,10 @@ namespace PKISharp.WACS.UnitTests.Tests.InstallationPluginTests plugins = new MockPluginService(log); } + /// <summary> + /// This tests only works when running as admin + /// </summary> + /// <returns></returns> [TestMethod] public async Task Regular() { diff --git a/src/main.test/Tests/RenewalTests/RenewalServiceTests.cs b/src/main.test/Tests/RenewalTests/RenewalServiceTests.cs index 98ce98e..943d2b5 100644 --- a/src/main.test/Tests/RenewalTests/RenewalServiceTests.cs +++ b/src/main.test/Tests/RenewalTests/RenewalServiceTests.cs @@ -25,7 +25,9 @@ namespace PKISharp.WACS.UnitTests.Tests.RenewalTests var argumentsService = new real.ArgumentsService(log, argumentsParser); var input = new mock.InputService(new List<string>() { - "y" // Confirm cancel all + "C", // Cancel command + "y", // Confirm cancel all + "Q" // Quit }); var builder = new ContainerBuilder(); @@ -71,7 +73,7 @@ namespace PKISharp.WACS.UnitTests.Tests.RenewalTests new TypedParameter(typeof(IContainer), container), new TypedParameter(typeof(RenewalExecutor), renewalExecutor)); Assert.IsNotNull(renewalManager); - renewalManager.CancelAllRenewals().Wait(); + renewalManager.ManageRenewals().Wait(); Assert.AreEqual(0, renewalStore.Renewals.Count()); } diff --git a/src/main/Program.cs b/src/main/Program.cs index f22f92f..078662d 100644 --- a/src/main/Program.cs +++ b/src/main/Program.cs @@ -8,7 +8,6 @@ using PKISharp.WACS.Plugins.Resolvers; using PKISharp.WACS.Services; using System; using System.Linq; -using System.Net; using System.Threading.Tasks; namespace PKISharp.WACS.Host @@ -127,6 +126,7 @@ namespace PKISharp.WACS.Host _ = builder.RegisterType<NotificationService>().SingleInstance(); _ = builder.RegisterType<RenewalExecutor>().SingleInstance(); _ = builder.RegisterType<RenewalManager>().SingleInstance(); + _ = builder.RegisterType<RenewalCreator>().SingleInstance(); _ = builder.Register(c => c.Resolve<IArgumentsService>().MainArguments).SingleInstance(); return builder.Build(); diff --git a/src/main/app.manifest b/src/main/app.manifest new file mode 100644 index 0000000..3616d9c --- /dev/null +++ b/src/main/app.manifest @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" /> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" /> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" /> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" /> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> + </application> + </compatibility> +</assembly> diff --git a/src/main/settings.json b/src/main/settings.json index dc06ae0..36b85bb 100644 --- a/src/main/settings.json +++ b/src/main/settings.json @@ -14,8 +14,8 @@ "DefaultBaseUriTest": "https://acme-staging-v02.api.letsencrypt.org/", "DefaultBaseUriImport": "https://acme-v01.api.letsencrypt.org/", "PostAsGet": true, - "RetryCount": 4, - "RetryInterval": 2 + "RetryCount": 5, + "RetryInterval": 5 }, "Proxy": { "Url": "[System]", diff --git a/src/main/wacs.csproj b/src/main/wacs.csproj index b563ec4..a7c3a71 100644 --- a/src/main/wacs.csproj +++ b/src/main/wacs.csproj @@ -9,6 +9,7 @@ <Version>2.1.2.0</Version> <AssemblyVersion>2.1.2.0</AssemblyVersion> <FileVersion>2.1.2.0</FileVersion> + <ApplicationManifest>app.manifest</ApplicationManifest> </PropertyGroup> <ItemGroup> diff --git a/src/plugin.validation.dns.cloudflare/Cloudflare.cs b/src/plugin.validation.dns.cloudflare/Cloudflare.cs index 0a8c3be..441eea2 100644 --- a/src/plugin.validation.dns.cloudflare/Cloudflare.cs +++ b/src/plugin.validation.dns.cloudflare/Cloudflare.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns { - public class Cloudflare : DnsValidation<Cloudflare> + public class Cloudflare : DnsValidation<Cloudflare>, IDisposable { private readonly CloudflareOptions _options; private readonly DomainParseService _domainParser; @@ -86,12 +86,11 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns .CallAsync(_hc) .ConfigureAwait(false); var record = records.FirstOrDefault(); - if (record != null) - { - await dns.Delete(record.Id) - .CallAsync(_hc) - .ConfigureAwait(false); - } + if (record == null) + throw new Exception($"The record {recordName} that should be deleted does not exist at Cloudflare."); + await dns.Delete(record.Id) + .CallAsync(_hc) + .ConfigureAwait(false); } public override async Task DeleteRecord(string recordName, string token) @@ -101,13 +100,6 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns await DeleteRecord(recordName, token, ctx, zone); } - protected override void Dispose(bool disposing) - { - if (disposing) - { - _hc.Dispose(); - } - base.Dispose(disposing); - } + public void Dispose() => _hc.Dispose(); } } diff --git a/src/plugin.validation.dns.cloudflare/CloudflareOptionsFactory.cs b/src/plugin.validation.dns.cloudflare/CloudflareOptionsFactory.cs index c6b9609..aa20344 100644 --- a/src/plugin.validation.dns.cloudflare/CloudflareOptionsFactory.cs +++ b/src/plugin.validation.dns.cloudflare/CloudflareOptionsFactory.cs @@ -22,14 +22,14 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns return opts; } - public override async Task<CloudflareOptions> Default(Target target) + public override Task<CloudflareOptions> Default(Target target) { var arg = _arguments.GetArguments<CloudflareArguments>(); var opts = new CloudflareOptions { ApiToken = new ProtectedString(_arguments.TryGetRequiredArgument(nameof(arg.CloudflareApiToken), arg.CloudflareApiToken)) }; - return opts; + return Task.FromResult(opts); } public override bool CanValidate(Target target) => true; diff --git a/src/plugin.validation.dns.cloudflare/wacs.validation.dns.cloudflare.csproj b/src/plugin.validation.dns.cloudflare/wacs.validation.dns.cloudflare.csproj index dee31df..3ac9d43 100644 --- a/src/plugin.validation.dns.cloudflare/wacs.validation.dns.cloudflare.csproj +++ b/src/plugin.validation.dns.cloudflare/wacs.validation.dns.cloudflare.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> @@ -7,7 +7,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="FluentCloudflare" Version="0.2.1" /> + <PackageReference Include="FluentCloudflare" Version="0.3.0" /> </ItemGroup> <ItemGroup> |