diff options
35 files changed, 771 insertions, 378 deletions
diff --git a/.gitmodules b/.gitmodules index 89b5017..03bddc5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "src/fluent-command-line-parser"]
path = src/fluent-command-line-parser
- url = https://github.com/WouterTinus/fluent-command-line-parser
+ url = https://github.com/win-acme/fluent-command-line-parser
[submodule "src/ACMESharpCore"] path = src/ACMESharpCore - url = https://github.com/WouterTinus/ACMESharpCore.git + url = https://github.com/win-acme/ACMESharpCore.git branch = win-acme diff --git a/src/main.lib/Clients/Acme/AcmeClient.cs b/src/main.lib/Clients/Acme/AcmeClient.cs index c13dda8..63320a9 100644 --- a/src/main.lib/Clients/Acme/AcmeClient.cs +++ b/src/main.lib/Clients/Acme/AcmeClient.cs @@ -13,6 +13,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Mail; +using System.Security.Authentication; using System.Security.Cryptography; using System.Threading.Tasks; @@ -217,18 +218,30 @@ namespace PKISharp.WACS.Clients.Acme /// </summary> internal async Task CheckNetwork() { - var httpClient = _proxyService.GetHttpClient(); + using var httpClient = _proxyService.GetHttpClient(); httpClient.BaseAddress = _settings.BaseUri; try { + _log.Verbose("SecurityProtocol setting: {setting}", System.Net.ServicePointManager.SecurityProtocol); _ = await httpClient.GetStringAsync("directory"); - _log.Debug("Connection OK!"); - } - catch (Exception ex) + } + catch (Exception) { - _log.Error(ex, "Error connecting to ACME server"); + _log.Warning("No luck yet, attempting to force TLS 1.2..."); + _proxyService.SslProtocols = SslProtocols.Tls12; + using var altClient = _proxyService.GetHttpClient(); + altClient.BaseAddress = _settings.BaseUri; + try + { + _ = await altClient.GetStringAsync("directory"); + } + catch (Exception ex) + { + _log.Error(ex, "Unable to connect to ACME server"); + return; + } } - + _log.Debug("Connection OK!"); } /// <summary> diff --git a/src/main.lib/Clients/IIS/IISClient.cs b/src/main.lib/Clients/IIS/IISClient.cs index 3ee4386..3171531 100644 --- a/src/main.lib/Clients/IIS/IISClient.cs +++ b/src/main.lib/Clients/IIS/IISClient.cs @@ -18,8 +18,10 @@ namespace PKISharp.WACS.Clients.IIS public Version Version { get; set; } [SuppressMessage("Code Quality", "IDE0069:Disposable fields should be disposed", Justification = "Actually is disposed")] - private ServerManager? _ServerManager; private readonly ILogService _log; + private ServerManager? _serverManager; + private List<IISSiteWrapper>? _webSites = null; + private List<IISSiteWrapper>? _ftpSites = null; public IISClient(ILogService log) { @@ -34,14 +36,16 @@ namespace PKISharp.WACS.Clients.IIS { get { - if (_ServerManager == null) + if (_serverManager == null) { if (Version.Major > 0) { - _ServerManager = new ServerManager(); + _serverManager = new ServerManager(); + _webSites = null; + _ftpSites = null; } } - return _ServerManager; + return _serverManager; } } @@ -53,11 +57,11 @@ namespace PKISharp.WACS.Clients.IIS /// </summary> private void Commit() { - if (_ServerManager != null) + if (_serverManager != null) { try { - _ServerManager.CommitChanges(); + _serverManager.CommitChanges(); } catch { @@ -73,18 +77,29 @@ namespace PKISharp.WACS.Clients.IIS public void Refresh() { - if (_ServerManager != null) + _webSites = null; + _ftpSites = null; + if (_serverManager != null) { - _ServerManager.Dispose(); - _ServerManager = null; + _serverManager.Dispose(); + _serverManager = null; } } #region _ Basic retrieval _ + IEnumerable<IIISSite> IIISClient.WebSites => WebSites; + + IEnumerable<IIISSite> IIISClient.FtpSites => FtpSites; + + IIISSite IIISClient.GetWebSite(long id) => GetWebSite(id); + + IIISSite IIISClient.GetFtpSite(long id) => GetFtpSite(id); + public bool HasWebSites => Version.Major > 0 && WebSites.Any(); - IEnumerable<IIISSite> IIISClient.WebSites => WebSites; + public bool HasFtpSites => Version >= new Version(7, 5) && FtpSites.Any(); + public IEnumerable<IISSiteWrapper> WebSites { get @@ -93,59 +108,65 @@ namespace PKISharp.WACS.Clients.IIS { return new List<IISSiteWrapper>(); } - return ServerManager.Sites.AsEnumerable(). - Where(s => s.Bindings.Any(sb => sb.Protocol == "http" || sb.Protocol == "https")). - Where(s => - { - try - { - return s.State == ObjectState.Started; - } - catch - { - // Prevent COMExceptions such as misconfigured - // application pools from crashing the whole - _log.Warning("Unable to determine state for Site {id}", s.Id); - return false; - } - }). - OrderBy(s => s.Name). - Select(x => new IISSiteWrapper(x)); + if (_webSites == null) + { + _webSites = ServerManager.Sites.AsEnumerable(). + Where(s => s.Bindings.Any(sb => sb.Protocol == "http" || sb.Protocol == "https")). + Where(s => + { + + try + { + return s.State == ObjectState.Started; + } + catch + { + // Prevent COMExceptions such as misconfigured + // application pools from crashing the whole + _log.Warning("Unable to determine state for Site {id}", s.Id); + return false; + } + }). + OrderBy(s => s.Name). + Select(x => new IISSiteWrapper(x)). + ToList(); + } + return _webSites; } } - IIISSite IIISClient.GetWebSite(long id) => GetWebSite(id); - public IISSiteWrapper GetWebSite(long id) + public IEnumerable<IISSiteWrapper> FtpSites { - foreach (var site in WebSites) + get { - if (site.Site.Id == id) + if (ServerManager == null) { - return site; + return new List<IISSiteWrapper>(); + } + if (_ftpSites == null) + { + _ftpSites = ServerManager.Sites.AsEnumerable(). + Where(s => s.Bindings.Any(sb => sb.Protocol == "ftp")). + OrderBy(s => s.Name). + Select(x => new IISSiteWrapper(x)). + ToList(); } + return _ftpSites; } - throw new Exception($"Unable to find IIS SiteId #{id}"); } - public bool HasFtpSites => Version >= new Version(7, 5) && FtpSites.Any(); - - IEnumerable<IIISSite> IIISClient.FtpSites => FtpSites; - public IEnumerable<IISSiteWrapper> FtpSites + public IISSiteWrapper GetWebSite(long id) { - get + foreach (var site in WebSites) { - if (ServerManager == null) + if (site.Site.Id == id) { - return new List<IISSiteWrapper>(); + return site; } - return ServerManager.Sites.AsEnumerable(). - Where(s => s.Bindings.Any(sb => sb.Protocol == "ftp")). - OrderBy(s => s.Name). - Select(x => new IISSiteWrapper(x)); } + throw new Exception($"Unable to find IIS SiteId #{id}"); } - IIISSite IIISClient.GetFtpSite(long id) => GetFtpSite(id); public IISSiteWrapper GetFtpSite(long id) { foreach (var site in FtpSites) @@ -316,9 +337,9 @@ namespace PKISharp.WACS.Clients.IIS { if (disposing) { - if (_ServerManager != null) + if (_serverManager != null) { - _ServerManager.Dispose(); + _serverManager.Dispose(); } } disposedValue = true; diff --git a/src/main.lib/Clients/IIS/IISHelper.cs b/src/main.lib/Clients/IIS/IISHelper.cs index 63a8649..7f28176 100644 --- a/src/main.lib/Clients/IIS/IISHelper.cs +++ b/src/main.lib/Clients/IIS/IISHelper.cs @@ -79,12 +79,17 @@ namespace PKISharp.WACS.Clients.IIS Where(sb => !string.IsNullOrWhiteSpace(sb.binding.Host)). ToList(); + static string lookupKey(IIISSite site, IIISBinding binding) => + site.Id + "#" + binding.BindingInformation.ToLower(); + // Option: hide http bindings when there are already https equivalents - var https = siteBindings.Where(sb => - sb.binding.Protocol == "https" || - sb.site.Bindings.Any(other => - other.Protocol == "https" && - string.Equals(sb.binding.Host, other.Host, StringComparison.InvariantCultureIgnoreCase))).ToList(); + var https = siteBindings + .Where(sb => + sb.binding.Protocol == "https" || + sb.site.Bindings.Any(other => + other.Protocol == "https" && + string.Equals(sb.binding.Host, other.Host, StringComparison.InvariantCultureIgnoreCase))) + .ToDictionary(sb => lookupKey(sb.site, sb.binding)); var targets = siteBindings. Select(sb => new @@ -92,7 +97,7 @@ namespace PKISharp.WACS.Clients.IIS host = sb.binding.Host.ToLower(), sb.site, sb.binding, - https = https.Contains(sb) + https = https.ContainsKey(lookupKey(sb.site, sb.binding)) }). Select(sbi => new IISBindingOption(sbi.host, _idnMapping.GetAscii(sbi.host)) { diff --git a/src/main.lib/Clients/IIS/IISHttpBindingUpdater.cs b/src/main.lib/Clients/IIS/IISHttpBindingUpdater.cs index 9e8f3bf..43945c3 100644 --- a/src/main.lib/Clients/IIS/IISHttpBindingUpdater.cs +++ b/src/main.lib/Clients/IIS/IISHttpBindingUpdater.cs @@ -61,9 +61,11 @@ namespace PKISharp.WACS.Clients.IIS { try { - UpdateBinding(site, binding, bindingOptions); found.Add(binding.Host); - bindingsUpdated += 1; + if (UpdateBinding(site, binding, bindingOptions)) + { + bindingsUpdated += 1; + } } catch (Exception ex) { @@ -91,7 +93,7 @@ namespace PKISharp.WACS.Clients.IIS var current = todo.First(); try { - var binding = AddOrUpdateBindings( + var (hostFound, commitRequired) = AddOrUpdateBindings( allBindings.Select(x => x.binding).ToArray(), targetSite, bindingOptions.WithHost(current)); @@ -99,7 +101,7 @@ namespace PKISharp.WACS.Clients.IIS // Allow a single newly created binding to match with // multiple hostnames on the todo list, e.g. the *.example.com binding // matches with both a.example.com and b.example.com - if (binding == null) + if (hostFound == null) { // We were unable to create the binding because it would // lead to a duplicate. Pretend that we did add it to @@ -108,8 +110,11 @@ namespace PKISharp.WACS.Clients.IIS } else { - found.Add(binding); - bindingsUpdated += 1; + found.Add(hostFound); + if (commitRequired) + { + bindingsUpdated += 1; + } } } catch (Exception ex) @@ -143,13 +148,16 @@ namespace PKISharp.WACS.Clients.IIS /// <param name="port"></param> /// <param name="ipAddress"></param> /// <param name="fuzzy"></param> - private string? AddOrUpdateBindings(TBinding[] allBindings, TSite site, BindingOptions bindingOptions) + private (string?, bool) AddOrUpdateBindings(TBinding[] allBindings, TSite site, BindingOptions bindingOptions) { if (bindingOptions.Host == null) { throw new InvalidOperationException("bindingOptions.Host is null"); } + // Require IIS manager to commit + var commitRequired = false; + // Get all bindings which could map to the host var matchingBindings = site.Bindings. Select(x => new { binding = x, fit = Fits(x.Host, bindingOptions.Host, bindingOptions.Flags) }). @@ -167,7 +175,7 @@ namespace PKISharp.WACS.Clients.IIS // All existing https bindings var existing = bestMatches. Where(x => x.binding.Protocol == "https"). - Select(x => x.binding.BindingInformation). + Select(x => x.binding.BindingInformation.ToLower()). ToList(); foreach (var match in bestMatches) @@ -178,7 +186,7 @@ namespace PKISharp.WACS.Clients.IIS if (UpdateExistingBindingFlags(bindingOptions.Flags, match.binding, allBindings, out var updateFlags)) { var updateOptions = bindingOptions.WithFlags(updateFlags); - UpdateBinding(site, match.binding, updateOptions); + commitRequired = UpdateBinding(site, match.binding, updateOptions); } } else @@ -194,14 +202,15 @@ namespace PKISharp.WACS.Clients.IIS } var binding = addOptions.Binding; - if (!existing.Contains(binding) && AllowAdd(addOptions, allBindings)) + if (!existing.Contains(binding.ToLower()) && AllowAdd(addOptions, allBindings)) { AddBinding(site, addOptions); existing.Add(binding); + commitRequired = true; } } } - return bestMatch.binding.Host; + return (bestMatch.binding.Host, commitRequired); } } @@ -210,11 +219,12 @@ namespace PKISharp.WACS.Clients.IIS if (AllowAdd(bindingOptions, allBindings)) { AddBinding(site, bindingOptions); - return bindingOptions.Host; + commitRequired = true; + return (bindingOptions.Host, commitRequired); } // We haven't been able to do anything - return null; + return (null, commitRequired); } /// <summary> @@ -243,7 +253,7 @@ namespace PKISharp.WACS.Clients.IIS // In general we shouldn't create duplicate bindings // because then only one of them will be usable at the // same time. - if (allBindings.Any(x => x.BindingInformation == bindingInfoFull)) + if (allBindings.Any(x => string.Equals(x.BindingInformation, bindingInfoFull, StringComparison.InvariantCultureIgnoreCase))) { _log.Warning($"Prevent adding duplicate binding for {bindingInfoFull}"); return false; @@ -365,7 +375,7 @@ namespace PKISharp.WACS.Clients.IIS _client.AddBinding(site, options); } - private void UpdateBinding(TSite site, TBinding existingBinding, BindingOptions options) + private bool UpdateBinding(TSite site, TBinding existingBinding, BindingOptions options) { // Check flags options = options.WithFlags(CheckFlags(false, existingBinding.Host, options.Flags)); @@ -377,6 +387,7 @@ namespace PKISharp.WACS.Clients.IIS string.Equals(existingBinding.CertificateStoreName, options.Store, StringComparison.InvariantCultureIgnoreCase)))) { _log.Verbose("No binding update needed"); + return false; } else { @@ -401,6 +412,7 @@ namespace PKISharp.WACS.Clients.IIS existingBinding.Port, (int)options.Flags); _client.UpdateBinding(site, existingBinding, options); + return true; } } diff --git a/src/main.lib/Clients/IIS/IISSiteWrapper.cs b/src/main.lib/Clients/IIS/IISSiteWrapper.cs index cb1ecd2..9bc204b 100644 --- a/src/main.lib/Clients/IIS/IISSiteWrapper.cs +++ b/src/main.lib/Clients/IIS/IISSiteWrapper.cs @@ -24,7 +24,7 @@ namespace PKISharp.WACS.Clients.IIS { Site = site; - Bindings = site.Bindings.Select(x => new IISBindingWrapper(x)); + Bindings = site.Bindings.Select(x => new IISBindingWrapper(x)).ToList(); } } diff --git a/src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtp.cs b/src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtp.cs index 1c66763..0896479 100644 --- a/src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtp.cs +++ b/src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtp.cs @@ -42,11 +42,12 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins internal static (bool, string?) Disabled(UserRoleService userRoleService, IIISClient iisClient) { - if (!userRoleService.AllowIIS.Item1) + var (allow, reason) = userRoleService.AllowIIS; + if (!allow) { - return (true, userRoleService.AllowIIS.Item2); + return (true, reason); } - if (!iisClient.HasWebSites) + if (!iisClient.HasFtpSites) { return (true, "No IIS ftp sites available."); } diff --git a/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWeb.cs b/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWeb.cs index e17dcfd..dd62c65 100644 --- a/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWeb.cs +++ b/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWeb.cs @@ -88,9 +88,10 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins internal static (bool, string?) Disabled(UserRoleService userRoleService, IIISClient iisClient) { - if (!userRoleService.AllowIIS.Item1) + var (allow, reason) = userRoleService.AllowIIS; + if (!allow) { - return (true, userRoleService.AllowIIS.Item2); + return (true, reason); } if (!iisClient.HasWebSites) { diff --git a/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs b/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs index 2796485..387c0b3 100644 --- a/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs +++ b/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs @@ -71,8 +71,7 @@ namespace PKISharp.WACS.Plugins.Resolvers x, description: x.Description, @default: x.GetType() == defaultType, - disabled: x.Disabled.Item1, - disabledReason: x.Disabled.Item2), + disabled: x.Disabled), "Abort"); return ret ?? new NullTargetFactory(); @@ -90,7 +89,7 @@ namespace PKISharp.WACS.Plugins.Resolvers _input.Show(null, "The ACME server will need to verify that you are the owner of the domain names that you are requesting" + " the certificate for. This happens both during initial setup *and* for every future renewal. There are two main methods of doing so: " + "answering specific http requests (http-01) or create specific dns records (dns-01). For wildcard domains the latter is the only option. " + - "Various additional plugins are available from https://github.com/PKISharp/win-acme/.", + "Various additional plugins are available from https://github.com/win-acme/win-acme/.", true); var options = _plugins.ValidationPluginFactories(scope). @@ -120,8 +119,7 @@ namespace PKISharp.WACS.Plugins.Resolvers x, description: $"[{x.ChallengeType}] {x.Description}", @default: x.GetType() == defaultType, - disabled: x.Disabled.Item1, - disabledReason: x.Disabled.Item2), + disabled: x.Disabled), "Abort"); return ret ?? new NullValidationFactory(); } @@ -165,8 +163,7 @@ namespace PKISharp.WACS.Plugins.Resolvers x, description: x.Description, @default: x is RsaOptionsFactory, - disabled: x.Disabled.Item1, - disabledReason: x.Disabled.Item2)); + disabled: x.Disabled)); return ret; } else @@ -218,8 +215,7 @@ namespace PKISharp.WACS.Plugins.Resolvers x, description: x.Description, @default: x.GetType() == defaultType, - disabled: x.Disabled.Item1, - disabledReason: x.Disabled.Item2), + disabled: x.Disabled), "Abort"); return store; @@ -285,8 +281,8 @@ namespace PKISharp.WACS.Plugins.Resolvers x => Choice.Create( x, description: x.plugin.Description, - disabled: !x.usable, - disabledReason: x.plugin.Disabled.Item1 ? x.plugin.Disabled.Item2 : "Incompatible with selected store.", + disabled: (!x.usable, x.plugin.Disabled.Item1 ? + x.plugin.Disabled.Item2 : "Incompatible with selected store."), @default: x.plugin.GetType() == @default)) ; return install.plugin; diff --git a/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs b/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs index 3105ef5..b0be55a 100644 --- a/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs +++ b/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs @@ -45,9 +45,10 @@ namespace PKISharp.WACS.Plugins.Resolvers _log.Error("Unable to find target plugin {PluginName}", _options.MainArguments.Target); return new NullTargetFactory(); } - if (targetPluginFactory.Disabled.Item1) + var (disabled, disabledReason) = targetPluginFactory.Disabled; + if (disabled) { - _log.Error($"Target plugin {{PluginName}} is not available. {targetPluginFactory.Disabled.Item2}", _options.MainArguments.Target); + _log.Error($"Target plugin {{PluginName}} is not available. {disabledReason}", _options.MainArguments.Target); return new NullTargetFactory(); } return targetPluginFactory; @@ -72,9 +73,10 @@ namespace PKISharp.WACS.Plugins.Resolvers _log.Error("Unable to find validation plugin {PluginName}", _options.MainArguments.Validation); return new NullValidationFactory(); } - if (validationPluginFactory.Disabled.Item1) + var (disabled, disabledReason) = validationPluginFactory.Disabled; + if (disabled) { - _log.Error($"Validation plugin {{PluginName}} is not available. {validationPluginFactory.Disabled.Item2}", validationPluginFactory.Name); + _log.Error($"Validation plugin {{PluginName}} is not available. {disabledReason}", validationPluginFactory.Name); return new NullValidationFactory(); } if (!validationPluginFactory.CanValidate(target)) @@ -112,9 +114,10 @@ namespace PKISharp.WACS.Plugins.Resolvers _log.Error("Unable to find installation plugin {PluginName}", name); return null; } - if (factory.Disabled.Item1) + var (disabled, disabledReason) = factory.Disabled; + if (disabled) { - _log.Error($"Installation plugin {{PluginName}} is not available. {factory.Disabled.Item2}", name); + _log.Error($"Installation plugin {{PluginName}} is not available. {disabledReason}", name); return null; } if (!factory.CanInstall(storeTypes)) @@ -164,9 +167,10 @@ namespace PKISharp.WACS.Plugins.Resolvers _log.Error("Unable to find store plugin {PluginName}", name); return null; } - if (factory.Disabled.Item1) + var (disabled, disabledReason) = factory.Disabled; + if (disabled) { - _log.Error($"Store plugin {{PluginName}} is not available. {factory.Disabled.Item2}", name); + _log.Error($"Store plugin {{PluginName}} is not available. {disabledReason}", name); return null; } return factory; @@ -184,18 +188,19 @@ namespace PKISharp.WACS.Plugins.Resolvers { return scope.Resolve<RsaOptionsFactory>(); } - var ret = _plugins.CsrPluginFactory(scope, pluginName); - if (ret == null) + var factory = _plugins.CsrPluginFactory(scope, pluginName); + if (factory == null) { _log.Error("Unable to find csr plugin {PluginName}", pluginName); return new NullCsrFactory(); } - if (ret.Disabled.Item1) + var (disabled, disabledReason) = factory.Disabled; + if (disabled) { - _log.Error($"CSR plugin {{PluginName}} is not available. {ret.Disabled.Item2}", pluginName); + _log.Error($"CSR plugin {{PluginName}} is not available. {disabledReason}", pluginName); return new NullCsrFactory(); } - return ret; + return factory; } } } diff --git a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs index 34cabbb..dc1405a 100644 --- a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs +++ b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs @@ -40,13 +40,14 @@ namespace PKISharp.WACS.Plugins.StorePlugins } } + private string PathForIdentifier(string identifier) => Path.Combine(_path, $"{identifier.Replace("*", "_")}.pfx"); + public Task Save(CertificateInfo input) { _log.Information("Copying certificate to the Central SSL store"); - IEnumerable<string> targets = input.HostNames; - foreach (var identifier in targets) + foreach (var identifier in input.HostNames) { - var dest = Path.Combine(_path, $"{identifier.Replace("*", "_")}.pfx"); + var dest = PathForIdentifier(identifier); _log.Information("Saving certificate to Central SSL location {dest}", dest); try { @@ -72,9 +73,10 @@ namespace PKISharp.WACS.Plugins.StorePlugins public Task Delete(CertificateInfo input) { _log.Information("Removing certificate from the Central SSL store"); - var di = new DirectoryInfo(_path); - foreach (var fi in di.GetFiles("*.pfx")) + foreach (var identifier in input.HostNames) { + var dest = PathForIdentifier(identifier); + var fi = new FileInfo(dest); var cert = LoadCertificate(fi); if (cert != null) { @@ -83,7 +85,7 @@ namespace PKISharp.WACS.Plugins.StorePlugins fi.Delete(); } cert.Dispose(); - } + } } return Task.CompletedTask; } @@ -96,6 +98,10 @@ namespace PKISharp.WACS.Plugins.StorePlugins private X509Certificate2? LoadCertificate(FileInfo fi) { X509Certificate2? cert = null; + if (!fi.Exists) + { + return cert; + } try { cert = new X509Certificate2(fi.FullName, _password); diff --git a/src/main.lib/Plugins/TargetPlugins/IIS/IIS.cs b/src/main.lib/Plugins/TargetPlugins/IIS/IIS.cs index bf9e0b9..7bda0e3 100644 --- a/src/main.lib/Plugins/TargetPlugins/IIS/IIS.cs +++ b/src/main.lib/Plugins/TargetPlugins/IIS/IIS.cs @@ -121,9 +121,10 @@ namespace PKISharp.WACS.Plugins.TargetPlugins internal static (bool, string?) Disabled(UserRoleService userRoleService) { - if (!userRoleService.AllowIIS.Item1) + var (allow, reason) = userRoleService.AllowIIS; + if (!allow) { - return (true, userRoleService.AllowIIS.Item2); + return (true, reason); } else { diff --git a/src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISBindingOptions.cs b/src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISBindingOptions.cs index 6663564..d3a1258 100644 --- a/src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISBindingOptions.cs +++ b/src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISBindingOptions.cs @@ -1,6 +1,6 @@ -using PKISharp.WACS.Plugins.Base; +using Newtonsoft.Json; +using PKISharp.WACS.Plugins.Base; using System.Collections.Generic; -using System.Linq; namespace PKISharp.WACS.Plugins.TargetPlugins { @@ -9,27 +9,13 @@ namespace PKISharp.WACS.Plugins.TargetPlugins { public long? SiteId { - get - { - if (IncludeSiteIds != null) - { - return IncludeSiteIds.FirstOrDefault(); - } - else - { - return null; - } - } + get => null; set { - if (value.HasValue) + if (IncludeSiteIds == null && value.HasValue) { IncludeSiteIds = new List<long>() { value.Value }; } - else - { - IncludeSiteIds = null; - } } } @@ -38,27 +24,13 @@ namespace PKISharp.WACS.Plugins.TargetPlugins /// </summary> public string? Host { - get - { - if (IncludeHosts != null) - { - return IncludeHosts.FirstOrDefault(); - } - else - { - return null; - } - } + get => null; set { - if (!string.IsNullOrEmpty(value)) + if (IncludeHosts == null && !string.IsNullOrEmpty(value)) { IncludeHosts = new List<string>() { value }; } - else - { - IncludeHosts = null; - } } } } diff --git a/src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISSiteOptions.cs b/src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISSiteOptions.cs index cfa0840..65fd2be 100644 --- a/src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISSiteOptions.cs +++ b/src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISSiteOptions.cs @@ -1,6 +1,5 @@ using PKISharp.WACS.Plugins.Base; using System.Collections.Generic; -using System.Linq; namespace PKISharp.WACS.Plugins.TargetPlugins { @@ -9,33 +8,25 @@ namespace PKISharp.WACS.Plugins.TargetPlugins { public long? SiteId { - get + get => null; + set { - if (IncludeSiteIds != null) - { - return IncludeSiteIds.FirstOrDefault(); - } - else + if (IncludeSiteIds == null && value.HasValue) { - return null; + IncludeSiteIds = new List<long>() { value.Value }; } } + } + + public List<string>? ExcludeBindings { + get => null; set { - if (value.HasValue) + if (ExcludeHosts == null) { - IncludeSiteIds = new List<long>() { value.Value }; - } - else - { - IncludeSiteIds = null; + ExcludeHosts = value; } } } - - public List<string>? ExcludeBindings { - get => ExcludeHosts; - set => ExcludeHosts = value; - } } } diff --git a/src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISSitesOptions.cs b/src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISSitesOptions.cs index c4e7e9c..0f8e53a 100644 --- a/src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISSitesOptions.cs +++ b/src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISSitesOptions.cs @@ -1,5 +1,4 @@ using PKISharp.WACS.Plugins.Base; -using PKISharp.WACS.Plugins.Base.Options; using System.Collections.Generic; namespace PKISharp.WACS.Plugins.TargetPlugins @@ -11,16 +10,33 @@ namespace PKISharp.WACS.Plugins.TargetPlugins /// Ignored, when this is false the other filter will be /// there, and when it's true there is no filter /// </summary> - public bool? All { get; set; } + public bool? All { + get => null; + set { } + } + public List<long>? SiteIds { - get => IncludeSiteIds; - set => IncludeSiteIds = value; + get => null; + set + { + if (IncludeSiteIds == null && value != null) + { + IncludeSiteIds = value; + } + } } + public List<string>? ExcludeBindings { - get => ExcludeHosts; - set => ExcludeHosts = value; + get => null; + set + { + if (ExcludeHosts == null) + { + ExcludeHosts = value; + } + } } } } diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs index 2ce7ab7..f8d5b0b 100644 --- a/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs +++ b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs @@ -39,7 +39,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http } } - protected override bool IsEmpty(string path) => !(new DirectoryInfo(path)).GetFileSystemInfos().Any(); + protected override bool IsEmpty(string path) => !new DirectoryInfo(path).EnumerateFileSystemInfos().Any(); protected override void WriteFile(string path, string content) { diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs index 4e18b1a..bc811fe 100644 --- a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs +++ b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs @@ -95,7 +95,11 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins { if (await _input.PromptYesNo("[--test] Try in default browser?", false)) { - Process.Start(Challenge.HttpResourceUrl); + Process.Start(new ProcessStartInfo + { + FileName = Challenge.HttpResourceUrl, + UseShellExecute = true + }); await _input.Wait(); } } @@ -167,12 +171,19 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins } if (_options.CopyWebConfig == true) { - _log.Debug("Writing web.config"); - var partialPath = Challenge.HttpResourcePath.Split('/').Last(); - var destination = CombinePath(_path, Challenge.HttpResourcePath.Replace(partialPath, "web.config")); - var content = GetWebConfig(); - WriteFile(destination, content); - _webConfigWritten = true; + try + { + _log.Debug("Writing web.config"); + var partialPath = Challenge.HttpResourcePath.Split('/').Last(); + var destination = CombinePath(_path, Challenge.HttpResourcePath.Replace(partialPath, "web.config")); + var content = GetWebConfig(); + WriteFile(destination, content); + _webConfigWritten = true; + } + catch (Exception ex) + { + _log.Warning("Unable to write web.config: {ex}", ex.Message); ; + } } } diff --git a/src/main.lib/RenewalCreator.cs b/src/main.lib/RenewalCreator.cs index 2928bfa..69131e5 100644 --- a/src/main.lib/RenewalCreator.cs +++ b/src/main.lib/RenewalCreator.cs @@ -116,9 +116,10 @@ namespace PKISharp.WACS _exceptionHandler.HandleException(message: $"No target plugin could be selected"); return; } - if (targetPluginOptionsFactory.Disabled.Item1) + var (targetPluginDisabled, targetPluginDisabledReason) = targetPluginOptionsFactory.Disabled; + if (targetPluginDisabled) { - _exceptionHandler.HandleException(message: $"Target plugin {targetPluginOptionsFactory.Name} is not available. {targetPluginOptionsFactory.Disabled.Item2}"); + _exceptionHandler.HandleException(message: $"Target plugin {targetPluginOptionsFactory.Name} is not available. {targetPluginDisabledReason}"); return; } var targetPluginOptions = runLevel.HasFlag(RunLevel.Unattended) ? diff --git a/src/main.lib/RenewalExecutor.cs b/src/main.lib/RenewalExecutor.cs index 70a1e12..94665dc 100644 --- a/src/main.lib/RenewalExecutor.cs +++ b/src/main.lib/RenewalExecutor.cs @@ -46,9 +46,10 @@ namespace PKISharp.WACS using var es = _scopeBuilder.Execution(ts, renewal, runLevel); // Generate the target var targetPlugin = es.Resolve<ITargetPlugin>(); - if (targetPlugin.Disabled.Item1) + var (disabled, disabledReason) = targetPlugin.Disabled; + if (disabled) { - throw new Exception($"Target plugin is not available. {targetPlugin.Disabled.Item2}"); + throw new Exception($"Target plugin is not available. {disabledReason}"); } var target = await targetPlugin.Generate(); if (target is INull) @@ -77,7 +78,7 @@ namespace PKISharp.WACS var cache = cs.CachedInfo(renewal, target); if (cache != null) { - _log.Information(LogType.All, "Renewal for {renewal} is due after {date}", renewal.LastFriendlyName, renewal.GetDueDate()); + _log.Information("Renewal for {renewal} is due after {date}", renewal.LastFriendlyName, renewal.GetDueDate()); return null; } else if (!renewal.New) @@ -98,19 +99,30 @@ namespace PKISharp.WACS // Create the order var client = es.Resolve<AcmeClient>(); var identifiers = target.GetHosts(false); + _log.Verbose("Creating certificate order for hosts: {identifiers}", identifiers); var order = await client.CreateOrder(identifiers); // Check if the order is valid - if (order.Payload.Status != AcmeClient.OrderReady && - order.Payload.Status != AcmeClient.OrderPending) + if ((order.Payload.Status != AcmeClient.OrderReady && + order.Payload.Status != AcmeClient.OrderPending) || + order.Payload.Error != null) { - return OnRenewFail(new Challenge() { Error = order.Payload.Error }); + _log.Error("Failed to create order {url}: {detail}", order.OrderUrl, order.Payload.Error.Detail); + return OnRenewFail(new Challenge() { Error = "Unable to create order" }); + } + else + { + _log.Verbose("Order {url} created", order.OrderUrl); } // Answer the challenges foreach (var authUrl in order.Payload.Authorizations) { // Get authorization details + _log.Verbose("Handle authorization {n}/{m}", + order.Payload.Authorizations.ToList().IndexOf(authUrl) + 1, + order.Payload.Authorizations.Length + 1); + var authorization = await client.GetAuthorizationDetails(authUrl); // Find a targetPart that matches the challenge @@ -145,11 +157,12 @@ namespace PKISharp.WACS var errors = challenge?.Error; if (errors != null) { - _log.Error("ACME server reported:"); - _log.Error("{@value}", errors); + return new RenewResult($"Authorization failed: {errors.ToString()}"); + } + else + { + return new RenewResult($"Authorization failed"); } - return new RenewResult("Authorization failed"); - } /// <summary> @@ -163,9 +176,13 @@ namespace PKISharp.WACS { var certificateService = renewalScope.Resolve<ICertificateService>(); var csrPlugin = target.CsrBytes == null ? renewalScope.Resolve<ICsrPlugin>() : null; - if (csrPlugin != null && csrPlugin.Disabled.Item1) + if (csrPlugin != null) { - return new RenewResult($"CSR plugin is not available. {csrPlugin.Disabled.Item2}"); + var (disabled, disabledReason) = csrPlugin.Disabled; + if (disabled) + { + return new RenewResult($"CSR plugin is not available. {disabledReason}"); + } } var oldCertificate = certificateService.CachedInfo(renewal); var newCertificate = await certificateService.RequestCertificate(csrPlugin, runLevel, renewal, target, order); @@ -208,9 +225,10 @@ namespace PKISharp.WACS { _log.Information("Store with {name}...", storeOptions.Name); } - if (storePlugin.Disabled.Item1) + var (disabled, disabledReason) = storePlugin.Disabled; + if (disabled) { - return new RenewResult($"Store plugin is not available. {storePlugin.Disabled.Item2}"); + return new RenewResult($"Store plugin is not available. {disabledReason}"); } await storePlugin.Save(newCertificate); storePlugins.Add(storePlugin); @@ -247,9 +265,10 @@ namespace PKISharp.WACS { _log.Information("Installing with {name}...", installOptions.Name); } - if (installPlugin.Disabled.Item1) + var (disabled, disabledReason) = installPlugin.Disabled; + if (disabled) { - return new RenewResult($"Installation plugin is not available. {installPlugin.Disabled.Item2}"); + return new RenewResult($"Installation plugin is not available. {disabledReason}"); } await installPlugin.Install(storePlugins, newCertificate, oldCertificate); } @@ -338,76 +357,123 @@ namespace PKISharp.WACS var valid = new Challenge { Status = AcmeClient.AuthorizationValid }; var client = execute.Resolve<AcmeClient>(); var identifier = authorization.Identifier.Value; + IValidationPlugin? validationPlugin = null; try { - _log.Information("Authorize identifier: {identifier}", identifier); - if (authorization.Status == AcmeClient.AuthorizationValid && - !runLevel.HasFlag(RunLevel.Test) && - !runLevel.HasFlag(RunLevel.IgnoreCache)) - { - _log.Information("Cached authorization result: {Status}", authorization.Status); - return valid; - } - else + if (authorization.Status == AcmeClient.AuthorizationValid) { - using var validation = _scopeBuilder.Validation(execute, options, targetPart, identifier); - IValidationPlugin? validationPlugin = null; - try + if (!runLevel.HasFlag(RunLevel.Test) && + !runLevel.HasFlag(RunLevel.IgnoreCache)) { - validationPlugin = validation.Resolve<IValidationPlugin>(); + _log.Information("Cached authorization result: {Status}", authorization.Status); + return valid; } - catch (Exception ex) + + if (runLevel.HasFlag(RunLevel.IgnoreCache)) { - _log.Error(ex, "Error resolving validation plugin"); + // Due to the IgnoreCache flag (--force switch) + // we are going to attempt to re-authorize the + // domain even though its already autorized. + // On failure, we can still use the cached result. + // This helps for migration scenarios. + invalid = valid; } - if (validationPlugin == null) - { - _log.Error("Validation plugin not found or not created."); - return invalid; - } - if (validationPlugin.Disabled.Item1) + } + + _log.Information("Authorize identifier: {identifier}", identifier); + _log.Verbose("Challenge types available: {challenges}", authorization.Challenges.Select(x => x.Type ?? "[Unknown]")); + var challenge = authorization.Challenges.FirstOrDefault(c => string.Equals(c.Type, options.ChallengeType, StringComparison.CurrentCultureIgnoreCase)); + if (challenge == null) + { + if (authorization.Status == AcmeClient.AuthorizationValid) { - _log.Error($"Validation plugin is not available. {validationPlugin.Disabled.Item2}"); - return invalid; - } - var challenge = authorization.Challenges.FirstOrDefault(c => string.Equals(c.Type, options.ChallengeType, StringComparison.CurrentCultureIgnoreCase)); - if (challenge == null) + var usedType = authorization.Challenges. + Where(x => x.Status == AcmeClient.AuthorizationValid). + FirstOrDefault(); + _log.Warning("Expected challenge type {type} not available for {identifier}, already validated using {valided}.", + options.ChallengeType, + authorization.Identifier.Value, + usedType?.Type ?? "[unknown]"); + return valid; + } + else { _log.Error("Expected challenge type {type} not available for {identifier}.", options.ChallengeType, authorization.Identifier.Value); + invalid.Error = "Expected challenge type not available"; return invalid; } + } - if (challenge.Status == AcmeClient.AuthorizationValid && - !runLevel.HasFlag(RunLevel.Test) && - !runLevel.HasFlag(RunLevel.IgnoreCache)) - { - _log.Information("{dnsIdentifier} already validated by {challengeType} validation ({name})", - authorization.Identifier.Value, - options.ChallengeType, - options.Name); - return valid; - } + // We actually have to do validation now + using var validation = _scopeBuilder.Validation(execute, options, targetPart, identifier); + try + { + validationPlugin = validation.Resolve<IValidationPlugin>(); + } + catch (Exception ex) + { + _log.Error(ex, "Error resolving validation plugin"); + } + if (validationPlugin == null) + { + _log.Error("Validation plugin not found or not created."); + invalid.Error = "Validation plugin not found or not created."; + return invalid; + } + var (disabled, disabledReason) = validationPlugin.Disabled; + if (disabled) + { + _log.Error($"Validation plugin is not available. {disabledReason}"); + invalid.Error = "Validation plugin is not available."; + return invalid; + } + _log.Information("Authorizing {dnsIdentifier} using {challengeType} validation ({name})", + identifier, + options.ChallengeType, + options.Name); + try + { + var details = await client.DecodeChallengeValidation(authorization, challenge); + await validationPlugin.PrepareChallenge(details); + } + catch (Exception ex) + { + _log.Error(ex, "Error preparing for challenge answer"); + invalid.Error = "Error preparing for challenge answer"; + return invalid; + } - _log.Information("Authorizing {dnsIdentifier} using {challengeType} validation ({name})", - identifier, - options.ChallengeType, - options.Name); - try - { - var details = await client.DecodeChallengeValidation(authorization, challenge); - await validationPlugin.PrepareChallenge(details); - } - catch (Exception ex) + _log.Debug("Submitting challenge answer"); + challenge = await client.AnswerChallenge(challenge); + if (challenge.Status != AcmeClient.AuthorizationValid) + { + if (challenge.Error != null) { - _log.Error(ex, "Error preparing for challenge answer"); - return invalid; + _log.Error(challenge.Error.ToString()); } - - _log.Debug("Submitting challenge answer"); - challenge = await client.AnswerChallenge(challenge); - + _log.Error("Authorization result: {Status}", challenge.Status); + invalid.Error = challenge.Error; + return invalid; + } + else + { + _log.Information("Authorization result: {Status}", challenge.Status); + return valid; + } + } + catch (Exception ex) + { + _log.Error("Error authorizing {renewal}", targetPart); + _exceptionHandler.HandleException(ex); + invalid.Error = ex.Message; + return invalid; + } + finally + { + if (validationPlugin != null) + { try { _log.Verbose("Starting post-validation cleanup"); @@ -418,31 +484,8 @@ namespace PKISharp.WACS { _log.Warning("An error occured during post-validation cleanup: {ex}", ex.Message); } - - if (challenge.Status != AcmeClient.AuthorizationValid) - { - if (challenge.Error != null) - { - _log.Error(challenge.Error.ToString()); - } - _log.Error("Authorization result: {Status}", challenge.Status); - return invalid; - } - else - { - _log.Information("Authorization result: {Status}", challenge.Status); - return valid; - } - - } } - catch (Exception ex) - { - _log.Error("Error authorizing {renewal}", targetPart); - _exceptionHandler.HandleException(ex); - return invalid; - } } } } diff --git a/src/main.lib/RenewalManager.cs b/src/main.lib/RenewalManager.cs index 1d6e71b..0410339 100644 --- a/src/main.lib/RenewalManager.cs +++ b/src/main.lib/RenewalManager.cs @@ -2,6 +2,7 @@ using PKISharp.WACS.Configuration; using PKISharp.WACS.DomainObjects; using PKISharp.WACS.Extensions; +using PKISharp.WACS.Plugins.Interfaces; using PKISharp.WACS.Plugins.TargetPlugins; using PKISharp.WACS.Services; using System; @@ -23,11 +24,13 @@ namespace PKISharp.WACS private readonly IAutofacBuilder _scopeBuilder; private readonly ExceptionHandler _exceptionHandler; private readonly RenewalExecutor _renewalExecutor; + private readonly ISettingsService _settings; public RenewalManager( IArgumentsService arguments, MainArguments args, IRenewalStore renewalStore, IContainer container, - IInputService input, ILogService log, + IInputService input, ILogService log, + ISettingsService settings, IAutofacBuilder autofacBuilder, ExceptionHandler exceptionHandler, RenewalExecutor renewalExecutor) { @@ -35,6 +38,7 @@ namespace PKISharp.WACS _args = args; _input = input; _log = log; + _settings = settings; _arguments = arguments; _container = container; _scopeBuilder = autofacBuilder; @@ -51,16 +55,17 @@ namespace PKISharp.WACS IEnumerable<Renewal> originalSelection = _renewalStore.Renewals.OrderBy(x => x.LastFriendlyName); var selectedRenewals = originalSelection; var quit = false; + var displayAll = false; do { var all = selectedRenewals.Count() == originalSelection.Count(); var none = selectedRenewals.Count() == 0; var totalLabel = originalSelection.Count() != 1 ? "renewals" : "renewal"; + var renewalSelectedLabel = selectedRenewals.Count() != 1 ? "renewals" : "renewal"; var selectionLabel = - all ? $"*all* renewals" : + all ? selectedRenewals.Count() == 1 ? "the renewal" : "*all* renewals" : none ? "no renewals" : $"{selectedRenewals.Count()} of {originalSelection.Count()} {totalLabel}"; - var renewalSelectedLabel = selectedRenewals.Count() != 1 ? "renewals" : "renewal"; _input.Show(null, "Welcome to the renewal manager. Actions selected in the menu below will " + @@ -69,36 +74,62 @@ namespace PKISharp.WACS "find what you're looking for.", true); - await _input.WritePagedList( - selectedRenewals.Select(x => Choice.Create<Renewal?>(x, + var displayRenewals = selectedRenewals; + var displayLimited = !displayAll && selectedRenewals.Count() >= _settings.UI.PageSize; + var displayHidden = 0; + var displayHiddenLabel = ""; + if (displayLimited) + { + displayRenewals = displayRenewals.Take(_settings.UI.PageSize - 1); + displayHidden = selectedRenewals.Count() - displayRenewals.Count(); + displayHiddenLabel = displayHidden != 1 ? "renewals" : "renewal"; + } + var choices = displayRenewals.Select(x => Choice.Create<Renewal?>(x, description: x.ToString(_input), - color: x.History.Last().Success ? + color: x.History.LastOrDefault()?.Success ?? false ? x.IsDue() ? ConsoleColor.DarkYellow : ConsoleColor.Green : - ConsoleColor.Red))); + ConsoleColor.Red)).ToList(); + if (displayLimited) + { + choices.Add(Choice.Create<Renewal?>(null, + command: "More", + description: $"{displayHidden} additional {displayHiddenLabel} selected but currently not displayed")); + } + await _input.WritePagedList(choices); + displayAll = false; 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, - disabledReason: "Not enough renewals to filter.", - @default: !(selectedRenewals.Count() < 2))); - options.Add( - Choice.Create<Func<Task>>( - async () => selectedRenewals = await SortRenewalsMenu(selectedRenewals), - "Sort renewals", "S", - @disabled: selectedRenewals.Count() < 2, - disabledReason: "Not enough renewals to sort.")); - options.Add( - Choice.Create<Func<Task>>( - () => { selectedRenewals = originalSelection; return Task.CompletedTask; }, - "Reset sorting and filtering", "X", - @disabled: all, - disabledReason: "No filters have been applied yet.", - @default: originalSelection.Count() > 0 && none)); + if (displayLimited) + { + options.Add( + Choice.Create<Func<Task>>( + () => { displayAll = true; return Task.CompletedTask; }, + "List all selected renewals", "A")); + } + + if (selectedRenewals.Count() > 1) + { + options.Add( + Choice.Create<Func<Task>>( + async () => selectedRenewals = await FilterRenewalsMenu(selectedRenewals), + all ? "Apply filter" : "Apply additional filter", "F", + @disabled: (selectedRenewals.Count() < 2, "Not enough renewals to filter."))); + options.Add( + Choice.Create<Func<Task>>( + async () => selectedRenewals = await SortRenewalsMenu(selectedRenewals), + "Sort renewals", "S", + @disabled: (selectedRenewals.Count() < 2, "Not enough renewals to sort."))); + } + if (!all) + { + options.Add( + Choice.Create<Func<Task>>( + () => { selectedRenewals = originalSelection; return Task.CompletedTask; }, + "Reset sorting and filtering", "X", + @disabled: (all, "No filters have been applied yet."))); + } options.Add( Choice.Create<Func<Task>>( async () => { @@ -123,8 +154,7 @@ namespace PKISharp.WACS } }, $"Show details for {selectionLabel}", "D", - @disabled: none, - disabledReason: "No renewals selected.")); + @disabled: (none, "No renewals selected."))); options.Add( Choice.Create<Func<Task>>( async () => { @@ -140,8 +170,12 @@ namespace PKISharp.WACS } }, $"Run {selectionLabel}", "R", - @disabled: none, - disabledReason: "No renewals selected.")); + @disabled: (none, "No renewals selected."))); + options.Add( + Choice.Create<Func<Task>>( + async () => selectedRenewals = await Analyze(selectedRenewals), + $"Analyze duplicates for {selectionLabel}", "A", + @disabled: (none, "No renewals selected."))); options.Add( Choice.Create<Func<Task>>( async () => { @@ -157,8 +191,7 @@ namespace PKISharp.WACS } }, $"Cancel {selectionLabel}", "C", - @disabled: none, - disabledReason: "No renewals selected.")); + @disabled: (none, "No renewals selected."))); options.Add( Choice.Create<Func<Task>>( async () => { @@ -181,24 +214,106 @@ namespace PKISharp.WACS }; } }, - $"Revoke {selectionLabel}", "V", - @disabled: none, - disabledReason: "No renewals selected.")); + $"Revoke certificate for {selectionLabel}", "V", + @disabled: (none, "No renewals selected."))); options.Add( Choice.Create<Func<Task>>( () => { quit = true; return Task.CompletedTask; }, "Back", "Q", @default: originalSelection.Count() == 0)); - - _input.Show(null, $"Currently selected {selectedRenewals.Count()} of {originalSelection.Count()} {totalLabel}", true); - var chosen = await _input.ChooseFromMenu("Please choose from the menu", options); + if (selectedRenewals.Count() > 1) + { + _input.Show(null, $"Currently selected {selectedRenewals.Count()} of {originalSelection.Count()} {totalLabel}", true); + } + var chosen = await _input.ChooseFromMenu( + "Choose an action or type numbers to select renewals", + options, + (string unexpected) => + Choice.Create<Func<Task>>( + async () => selectedRenewals = await FilterRenewalsById(selectedRenewals, unexpected))); await chosen.Invoke(); } while (!quit); } /// <summary> + /// Check if there are multiple renewals installing to the same site + /// or requesting certificates for the same domains + /// </summary> + /// <param name="selectedRenewals"></param> + /// <returns></returns> + private async Task<IEnumerable<Renewal>> Analyze(IEnumerable<Renewal> selectedRenewals) + { + var foundHosts = new Dictionary<string, List<Renewal>>(); + var foundSites = new Dictionary<long, List<Renewal>>(); + + foreach (var renewal in selectedRenewals) + { + using var targetScope = _scopeBuilder.Target(_container, renewal, RunLevel.Unattended); + var target = targetScope.Resolve<Target>(); + foreach (var targetPart in target.Parts) + { + if (targetPart.SiteId != null) + { + var siteId = targetPart.SiteId.Value; + if (!foundSites.ContainsKey(siteId)) + { + foundSites.Add(siteId, new List<Renewal>()); + } + foundSites[siteId].Add(renewal); + } + foreach (var host in targetPart.GetHosts(true)) + { + if (!foundHosts.ContainsKey(host)) + { + foundHosts.Add(host, new List<Renewal>()); + } + foundHosts[host].Add(renewal); + } + } + } + + // List results + var options = new List<Choice<List<Renewal>>>(); + foreach (var site in foundSites) + { + if (site.Value.Count() > 1) + { + options.Add( + Choice.Create( + site.Value, + $"Select {site.Value.Count()} renewals covering IIS site {site.Key}")); + } + } + foreach (var host in foundHosts) + { + if (host.Value.Count() > 1) + { + options.Add( + Choice.Create( + host.Value, + $"Select {host.Value.Count()} renewals covering host {host.Key}")); + } + } + if (options.Count == 0) + { + _input.Show(null, "Analysis didn't find any overlap between renewals.", first: true); + return selectedRenewals; + } + else + { + options.Add( + Choice.Create( + selectedRenewals.ToList(), + $"Back")); + _input.Show(null, "Analysis found some overlap between renewals. You can select the overlapping renewals from the menu.", first: true); + return await _input.ChooseFromMenu("Please choose from the menu", options); + } + + } + + /// <summary> /// Offer user different ways to sort the renewals /// </summary> /// <param name="current"></param> @@ -235,10 +350,6 @@ namespace PKISharp.WACS var options = new List<Choice<Func<Task<IEnumerable<Renewal>>>>> { 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>>>>( @@ -269,7 +380,12 @@ namespace PKISharp.WACS private async Task<IEnumerable<Renewal>> FilterRenewalsById(IEnumerable<Renewal> current) { var rawInput = await _input.RequestString("Please input the list index of the renewal(s) you'd like to select"); - var parts = rawInput.ParseCsv(); + return await FilterRenewalsById(current, rawInput); + } + + private async Task<IEnumerable<Renewal>> FilterRenewalsById(IEnumerable<Renewal> current, string input) + { + var parts = input.ParseCsv(); if (parts == null) { return current; @@ -282,12 +398,12 @@ namespace PKISharp.WACS if (index > 0 && index <= current.Count()) { ret.Add(current.ElementAt(index - 1)); - } + } else { _log.Warning("Input out of range: {part}", part); } - } + } else { _log.Warning("Invalid input: {part}", part); diff --git a/src/main.lib/Services/ArgumentsService.cs b/src/main.lib/Services/ArgumentsService.cs index 4601fc6..685ca55 100644 --- a/src/main.lib/Services/ArgumentsService.cs +++ b/src/main.lib/Services/ArgumentsService.cs @@ -8,8 +8,19 @@ namespace PKISharp.WACS.Services { private readonly ILogService _log; private readonly ArgumentsParser _parser; + private MainArguments? _mainArguments; - public MainArguments MainArguments => _parser.GetArguments<MainArguments>(); + public MainArguments MainArguments + { + get + { + if (_mainArguments == null) + { + _mainArguments = _parser.GetArguments<MainArguments>(); + } + return _mainArguments; + } + } public ArgumentsService(ILogService log, ArgumentsParser parser) { diff --git a/src/main.lib/Services/CertificateService.cs b/src/main.lib/Services/CertificateService.cs index d30e601..52f7d11 100644 --- a/src/main.lib/Services/CertificateService.cs +++ b/src/main.lib/Services/CertificateService.cs @@ -88,7 +88,7 @@ namespace PKISharp.WACS.Services /// <param name="renewal"></param> private void ClearCache(Renewal renewal, string prefix = "*", string postfix = "*") { - foreach (var f in _cache.GetFiles($"{prefix}{renewal.Id}{postfix}")) + foreach (var f in _cache.EnumerateFiles($"{prefix}{renewal.Id}{postfix}")) { _log.Verbose("Deleting {file} from {folder}", f.Name, _cache.FullName); try @@ -108,7 +108,7 @@ namespace PKISharp.WACS.Services /// </summary> public void Encrypt() { - foreach (var f in _cache.GetFiles($"*.keys")) + foreach (var f in _cache.EnumerateFiles($"*.keys")) { var x = new ProtectedString(File.ReadAllText(f.FullName)); _log.Information("Rewriting {x}", f.Name); @@ -135,7 +135,7 @@ namespace PKISharp.WACS.Services var nameAll = GetPath(renewal, ""); var directory = new DirectoryInfo(Path.GetDirectoryName(nameAll)); var allPattern = Path.GetFileName(nameAll); - var allFiles = directory.GetFiles(allPattern + "*"); + var allFiles = directory.EnumerateFiles(allPattern + "*"); if (!allFiles.Any()) { return null; diff --git a/src/main.lib/Services/InputService.cs b/src/main.lib/Services/InputService.cs index 0dd4ed2..06416d3 100644 --- a/src/main.lib/Services/InputService.cs +++ b/src/main.lib/Services/InputService.cs @@ -339,7 +339,7 @@ namespace PKISharp.WACS.Services /// Print a (paged) list of choices for the user to choose from /// </summary> /// <param name="choices"></param> - public async Task<T> ChooseFromMenu<T>(string what, List<Choice<T>> choices) + public async Task<T> ChooseFromMenu<T>(string what, List<Choice<T>> choices, Func<string, Choice<T>>? unexpected = null) { if (!choices.Any()) { @@ -379,6 +379,11 @@ namespace PKISharp.WACS.Services _log.Warning($"The option you have chosen is currently disabled. {disabledReason}"); selected = null; } + + if (selected == null && unexpected != null) + { + selected = unexpected(choice); + } } } while (selected == null); return selected.Item; diff --git a/src/main.lib/Services/Interfaces/IInputService.cs b/src/main.lib/Services/Interfaces/IInputService.cs index 78d155c..b59f82c 100644 --- a/src/main.lib/Services/Interfaces/IInputService.cs +++ b/src/main.lib/Services/Interfaces/IInputService.cs @@ -8,7 +8,7 @@ namespace PKISharp.WACS.Services { Task<TResult?> ChooseOptional<TSource, TResult>(string what, IEnumerable<TSource> options, Func<TSource, Choice<TResult?>> creator, string nullChoiceLabel) where TResult : class; Task<TResult> ChooseRequired<TSource, TResult>(string what, IEnumerable<TSource> options, Func<TSource, Choice<TResult>> creator); - Task<TResult> ChooseFromMenu<TResult>(string what, List<Choice<TResult>> choices); + Task<TResult> ChooseFromMenu<TResult>(string what, List<Choice<TResult>> choices, Func<string, Choice<TResult>>? unexpected = null); Task<bool> PromptYesNo(string message, bool defaultOption); Task<string?> ReadPassword(string what); Task<string> RequestString(string what); @@ -27,11 +27,14 @@ namespace PKISharp.WACS.Services string? description = null, string? command = null, bool @default = false, - bool disabled = false, - string? disabledReason = null, + (bool, string?)? disabled = null, ConsoleColor? color = null) { var newItem = new Choice<TItem>(item); + if (disabled == null) + { + disabled = (false, null); + } // Default description is item.ToString, but it may // be overruled by the optional parameter here if (!string.IsNullOrEmpty(description)) @@ -40,8 +43,8 @@ namespace PKISharp.WACS.Services } newItem.Command = command; newItem.Color = color; - newItem.Disabled = disabled; - newItem.DisabledReason = disabledReason; + newItem.Disabled = disabled.Value.Item1; + newItem.DisabledReason = disabled.Value.Item2; newItem.Default = @default; return newItem; } diff --git a/src/main.lib/Services/LogService.cs b/src/main.lib/Services/LogService.cs index 3b6fde5..e88997d 100644 --- a/src/main.lib/Services/LogService.cs +++ b/src/main.lib/Services/LogService.cs @@ -91,6 +91,7 @@ namespace PKISharp.WACS.Services { var defaultPath = path.TrimEnd('\\', '/') + "\\log-.txt"; var defaultRollingInterval = RollingInterval.Day; + var defaultRetainedFileCountLimit = 120; var fileConfig = new ConfigurationBuilder() .AddJsonFile(_configurationPath, true, true) .Build(); @@ -104,6 +105,11 @@ namespace PKISharp.WACS.Services { pathSection.Value = defaultPath; } + var retainedFileCountLimit = writeTo.GetSection("Args:retainedFileCountLimit"); + if (string.IsNullOrEmpty(retainedFileCountLimit.Value)) + { + retainedFileCountLimit.Value = defaultRetainedFileCountLimit.ToString(); + } var rollingInterval = writeTo.GetSection("Args:rollingInterval"); if (string.IsNullOrEmpty(rollingInterval.Value)) { @@ -116,7 +122,10 @@ namespace PKISharp.WACS.Services .MinimumLevel.ControlledBy(_levelSwitch) .Enrich.FromLogContext() .Enrich.WithProperty("ProcessId", Process.GetCurrentProcess().Id) - .WriteTo.File(defaultPath, rollingInterval: defaultRollingInterval) + .WriteTo.File( + defaultPath, + rollingInterval: defaultRollingInterval, + retainedFileCountLimit: defaultRetainedFileCountLimit) .ReadFrom.Configuration(fileConfig, "disk") .CreateLogger(); } diff --git a/src/main.lib/Services/PluginService.cs b/src/main.lib/Services/PluginService.cs index 2f21930..fff7fec 100644 --- a/src/main.lib/Services/PluginService.cs +++ b/src/main.lib/Services/PluginService.cs @@ -195,7 +195,7 @@ namespace PKISharp.WACS.Services } var installDir = new FileInfo(Process.GetCurrentProcess().MainModule.FileName).Directory; - var dllFiles = installDir.GetFiles("*.dll", SearchOption.AllDirectories); + var dllFiles = installDir.EnumerateFiles("*.dll", SearchOption.AllDirectories); #if PLUGGABLE var allAssemblies = new List<Assembly>(); foreach (var file in dllFiles) diff --git a/src/main.lib/Services/ProxyService.cs b/src/main.lib/Services/ProxyService.cs index 22e0353..f2d49a9 100644 --- a/src/main.lib/Services/ProxyService.cs +++ b/src/main.lib/Services/ProxyService.cs @@ -1,6 +1,7 @@ using System; using System.Net; using System.Net.Http; +using System.Security.Authentication; namespace PKISharp.WACS.Services { @@ -9,6 +10,7 @@ namespace PKISharp.WACS.Services private readonly ILogService _log; private IWebProxy? _proxy; private readonly ISettingsService _settings; + public SslProtocols SslProtocols { get; set; } = SslProtocols.None; public ProxyService(ILogService log, ISettingsService settings) { @@ -29,7 +31,8 @@ namespace PKISharp.WACS.Services { var httpClientHandler = new HttpClientHandler() { - Proxy = GetWebProxy() + Proxy = GetWebProxy(), + SslProtocols = SslProtocols }; if (!checkSsl) { diff --git a/src/main.lib/Services/RenewalStoreDisk.cs b/src/main.lib/Services/RenewalStoreDisk.cs index ce1d6e9..1931f63 100644 --- a/src/main.lib/Services/RenewalStoreDisk.cs +++ b/src/main.lib/Services/RenewalStoreDisk.cs @@ -34,20 +34,25 @@ namespace PKISharp.WACS.Services var list = new List<Renewal>(); var di = new DirectoryInfo(_settings.Client.ConfigurationPath); var postFix = ".renewal.json"; - foreach (var rj in di.GetFiles($"*{postFix}", SearchOption.AllDirectories)) + foreach (var rj in di.EnumerateFiles($"*{postFix}", SearchOption.AllDirectories)) { try { var storeConverter = new PluginOptionsConverter<StorePluginOptions>(_plugin.PluginOptionTypes<StorePluginOptions>(), _log); var result = JsonConvert.DeserializeObject<Renewal>( File.ReadAllText(rj.FullName), - new ProtectedStringConverter(_log, _settings), - new StorePluginOptionsConverter(storeConverter), - new PluginOptionsConverter<TargetPluginOptions>(_plugin.PluginOptionTypes<TargetPluginOptions>(), _log), - new PluginOptionsConverter<CsrPluginOptions>(_plugin.PluginOptionTypes<CsrPluginOptions>(), _log), - storeConverter, - new PluginOptionsConverter<ValidationPluginOptions>(_plugin.PluginOptionTypes<ValidationPluginOptions>(), _log), - new PluginOptionsConverter<InstallationPluginOptions>(_plugin.PluginOptionTypes<InstallationPluginOptions>(), _log)); + new JsonSerializerSettings() { + ObjectCreationHandling = ObjectCreationHandling.Replace, + Converters = { + new ProtectedStringConverter(_log, _settings), + new StorePluginOptionsConverter(storeConverter), + new PluginOptionsConverter<TargetPluginOptions>(_plugin.PluginOptionTypes<TargetPluginOptions>(), _log), + new PluginOptionsConverter<CsrPluginOptions>(_plugin.PluginOptionTypes<CsrPluginOptions>(), _log), + storeConverter, + new PluginOptionsConverter<ValidationPluginOptions>(_plugin.PluginOptionTypes<ValidationPluginOptions>(), _log), + new PluginOptionsConverter<InstallationPluginOptions>(_plugin.PluginOptionTypes<InstallationPluginOptions>(), _log) + } + }); if (result == null) { throw new Exception("result is empty"); diff --git a/src/main.lib/Services/SettingsService.cs b/src/main.lib/Services/SettingsService.cs index 3b4219c..03aad0f 100644 --- a/src/main.lib/Services/SettingsService.cs +++ b/src/main.lib/Services/SettingsService.cs @@ -31,26 +31,42 @@ namespace PKISharp.WACS.Services _log = log; _arguments = arguments; - var installDir = new FileInfo(ExePath).DirectoryName; - _log.Verbose($"Looking for settings.json in {installDir}"); - var settings = new FileInfo(Path.Combine(installDir, "settings.json")); - var settingsTemplate = new FileInfo(Path.Combine(installDir, "settings_default.json")); + var installDir = new FileInfo(ExePath).DirectoryName; + var settingsFileName = "settings.json"; + var settingsFileTemplateName = "settings_default.json"; + _log.Verbose($"Looking for {settingsFileName} in {installDir}"); + var settings = new FileInfo(Path.Combine(installDir, settingsFileName)); + var settingsTemplate = new FileInfo(Path.Combine(installDir, settingsFileTemplateName)); + var useFile = settings; if (!settings.Exists && settingsTemplate.Exists) { - _log.Verbose($"Copying settings_default.json to settings.json"); - settingsTemplate.CopyTo(settings.FullName); + _log.Verbose($"Copying {settingsFileTemplateName} to {settingsFileName}"); + try + { + settingsTemplate.CopyTo(settings.FullName); + } + catch (Exception) + { + _log.Error($"Unable to create {settingsFileName}, falling back to {settingsFileTemplateName}"); + useFile = settingsTemplate; + } } try { new ConfigurationBuilder() - .AddJsonFile(Path.Combine(installDir, "settings.json"), true, true) + .AddJsonFile(useFile.FullName, true, true) .Build() .Bind(this); } catch (Exception ex) { - _log.Error(new Exception("Invalid settings.json", ex), "Unable to start program"); + _log.Error($"Unable to start program using {useFile.Name}"); + while (ex.InnerException != null) + { + _log.Error(ex.InnerException.Message); + ex = ex.InnerException; + } return; } diff --git a/src/main.lib/Wacs.cs b/src/main.lib/Wacs.cs index 6c5efee..96244be 100644 --- a/src/main.lib/Wacs.cs +++ b/src/main.lib/Wacs.cs @@ -8,6 +8,7 @@ using PKISharp.WACS.Services; using PKISharp.WACS.Services.Legacy; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -210,7 +211,7 @@ namespace PKISharp.WACS.Host _log.Warning("Running without administrator credentials, some options disabled"); } _taskScheduler.ConfirmTaskScheduler(); - _log.Information("Please report issues at {url}", "https://github.com/PKISharp/win-acme"); + _log.Information("Please report issues at {url}", "https://github.com/win-acme/win-acme"); _log.Verbose("Test for international support: {chinese} {russian} {arab}", "語言", "язык", "لغة"); } @@ -235,31 +236,27 @@ namespace PKISharp.WACS.Host var total = _renewalStore.Renewals.Count(); var due = _renewalStore.Renewals.Count(x => x.IsDue()); var error = _renewalStore.Renewals.Count(x => !x.History.Last().Success); - + var (allowIIS, allowIISReason) = _userRoleService.AllowIIS; var options = new List<Choice<Func<Task>>> { Choice.Create<Func<Task>>( () => _renewalCreator.SetupRenewal(RunLevel.Interactive | RunLevel.Simple), "Create new certificate (simple for IIS)", "N", - @default: _userRoleService.AllowIIS.Item1, - disabled: !_userRoleService.AllowIIS.Item1, - disabledReason: _userRoleService.AllowIIS.Item2), + @default: allowIIS, + disabled: (!allowIIS, allowIISReason)), Choice.Create<Func<Task>>( () => _renewalCreator.SetupRenewal(RunLevel.Interactive | RunLevel.Advanced), "Create new certificate (full options)", "M", - @default: !_userRoleService.AllowIIS.Item1), + @default: !allowIIS), Choice.Create<Func<Task>>( () => _renewalManager.CheckRenewals(RunLevel.Interactive), $"Run scheduled renewals ({due} currently due)", "R", - color: due == 0 ? (ConsoleColor?)null : ConsoleColor.Yellow, - disabled: due == 0, - disabledReason: "No renewals are currently due"), + color: due == 0 ? (ConsoleColor?)null : ConsoleColor.Yellow), Choice.Create<Func<Task>>( () => _renewalManager.ManageRenewals(), $"Manage renewals ({total} total{(error == 0 ? "" : $", {error} in error")})", "A", color: error == 0 ? (ConsoleColor?)null : ConsoleColor.Red, - disabled: total == 0, - disabledReason: "No renewals have been created yet."), + disabled: (total == 0, "No renewals have been created yet.")), Choice.Create<Func<Task>>( () => ExtraMenu(), "More options...", "O"), @@ -281,8 +278,8 @@ namespace PKISharp.WACS.Host Choice.Create<Func<Task>>( () => _taskScheduler.EnsureTaskScheduler(RunLevel.Interactive | RunLevel.Advanced, true), "(Re)create scheduled task", "T", - disabled: !_userRoleService.AllowTaskScheduler, - disabledReason: "Run as an administrator to allow access to the task scheduler."), + disabled: (!_userRoleService.AllowTaskScheduler, + "Run as an administrator to allow access to the task scheduler.")), Choice.Create<Func<Task>>( () => _container.Resolve<EmailClient>().Test(), "Test email notification", "E"), @@ -292,8 +289,8 @@ namespace PKISharp.WACS.Host Choice.Create<Func<Task>>( () => Import(RunLevel.Interactive | RunLevel.Advanced), "Import scheduled renewals from WACS/LEWS 1.9.x", "I", - disabled: !_userRoleService.IsAdmin, - disabledReason: "Run as an administrator to allow search for legacy renewals."), + disabled: (!_userRoleService.IsAdmin, + "Run as an administrator to allow search for legacy renewals.")), Choice.Create<Func<Task>>( () => Encrypt(RunLevel.Interactive), "Encrypt/decrypt configuration", "M"), @@ -353,7 +350,7 @@ namespace PKISharp.WACS.Host _input.Show(null, " 4. On the new machine, set the EncryptConfig setting to true"); _input.Show(null, " 5. Run this option; all unprotected values will be saved with protection"); _input.Show(null, $"Data directory: {settings.Client.ConfigurationPath}", true); - _input.Show(null, $"Config directory: {settings.ExePath}\\settings.json"); + _input.Show(null, $"Config directory: {new FileInfo(settings.ExePath).Directory.FullName}\\settings.json"); _input.Show(null, $"Current EncryptConfig setting: {encryptConfig}"); userApproved = await _input.PromptYesNo($"Save all renewal files {(encryptConfig ? "with" : "without")} encryption?", false); } diff --git a/src/main.test/Mock/Clients/IISClient.cs b/src/main.test/Mock/Clients/IISClient.cs index c2a5898..f131434 100644 --- a/src/main.test/Mock/Clients/IISClient.cs +++ b/src/main.test/Mock/Clients/IISClient.cs @@ -164,7 +164,22 @@ namespace PKISharp.WACS.UnitTests.Mock.Clients public string IP { get; set; } public byte[] CertificateHash { get; set; } public string CertificateStoreName { get; set; } - public string BindingInformation => $"{IP}:{Port}:{Host}"; + public string BindingInformation + { + get + { + if (_bindingInformation != null) + { + return _bindingInformation; + } + else + { + return $"{IP}:{Port}:{Host}"; + } + } + set => _bindingInformation = value; + } + private string? _bindingInformation = null; public SSLFlags SSLFlags { get; set; } } } diff --git a/src/main.test/Mock/Services/InputService.cs b/src/main.test/Mock/Services/InputService.cs index 3771cab..58e8d2b 100644 --- a/src/main.test/Mock/Services/InputService.cs +++ b/src/main.test/Mock/Services/InputService.cs @@ -43,25 +43,31 @@ namespace PKISharp.WACS.UnitTests.Mock.Services FirstOrDefault(c => string.Equals(c.Command, input, StringComparison.CurrentCultureIgnoreCase)).Item); } - public Task<TResult> ChooseFromMenu<TResult>(string what, List<Choice<TResult>> choices) - { - var input = GetNextInput(); - return Task. - FromResult(choices. - FirstOrDefault(c => string.Equals(c.Command, input, StringComparison.CurrentCultureIgnoreCase)).Item); - } - public string FormatDate(DateTime date) => ""; public Task<bool> PromptYesNo(string message, bool defaultOption) { var input = GetNextInput(); return Task.FromResult(string.Equals(input, "y", StringComparison.CurrentCultureIgnoreCase)); } - public Task<string> ReadPassword(string what) => Task.FromResult(GetNextInput()); + public Task<string?> ReadPassword(string what) => Task.FromResult(GetNextInput()); public Task<string> RequestString(string what) => Task.FromResult(GetNextInput()); public Task<string> RequestString(string[] what) => Task.FromResult(GetNextInput()); - public void Show(string label, string value = null, bool first = false, int level = 0) { } + public void Show(string? label, string? value = null, bool first = false, int level = 0) { } public Task<bool> Wait(string message = "") => Task.FromResult(true); public Task WritePagedList(IEnumerable<Choice> listItems) => Task.CompletedTask; + public Task<TResult> ChooseFromMenu<TResult>(string what, List<Choice<TResult>> choices, Func<string, Choice<TResult>>? unexpected = null) + { + var input = GetNextInput(); + var choice = choices.FirstOrDefault(c => string.Equals(c.Command, input, StringComparison.CurrentCultureIgnoreCase)); + if (choice == null && unexpected != null) + { + choice = unexpected(input); + } + if (choice != null) + { + return Task.FromResult(choice.Item); + } + throw new Exception(); + } } } diff --git a/src/main.test/Tests/BindingTests/Bindings.cs b/src/main.test/Tests/BindingTests/Bindings.cs index 1343924..ac4a157 100644 --- a/src/main.test/Tests/BindingTests/Bindings.cs +++ b/src/main.test/Tests/BindingTests/Bindings.cs @@ -973,5 +973,56 @@ namespace PKISharp.WACS.UnitTests.Tests.BindingTests Assert.AreEqual(iis.WebSites.First().Bindings.First().CertificateHash , newCert); Assert.AreEqual(iis.WebSites.First().Bindings.Last().CertificateHash, newCert); } + + [TestMethod] + [DataRow("UPPERCASE.example.com", "UPPERCASE.example.com", "UPPERCASE.example.com")] + [DataRow("uppercase.example.com", "UPPERCASE.example.com", "UPPERCASE.example.com")] + [DataRow("UPPERCASE.example.com", "uppercase.example.com", "UPPERCASE.example.com")] + [DataRow("UPPERCASE.example.com", "UPPERCASE.example.com", "uppercase.example.com")] + [DataRow("UPPERCASE.example.com", "uppercase.example.com", "uppercase.example.com")] + [DataRow("uppercase.example.com", "UPPERCASE.example.com", "uppercase.example.com")] + [DataRow("uppercase.example.com", "uppercase.example.com", "UPPERCASE.example.com")] + [DataRow("uppercase.example.com", "uppercase.example.com", "uppercase.example.com")] + public void UppercaseBinding(string host, string bindingInfo, string newHost) + { + var mockBinding = new MockBinding() + { + IP = "*", + Port = 443, + Host = host, + Protocol = "https", + CertificateHash = oldCert1, + CertificateStoreName = DefaultStore + }; + mockBinding.BindingInformation = $"*:443:{bindingInfo}"; + + var dup1 = new MockSite() + { + Id = 1, + Bindings = new List<MockBinding> { + mockBinding, + new MockBinding() + { + IP = "*", + Port = 80, + Host = host, + Protocol = "http" + } + } + }; + + var iis = new MockIISClient(log, 10) + { + MockSites = new[] { dup1 } + }; + + var bindingOptions = new BindingOptions(). + WithSiteId(1). + WithStore(DefaultStore). + WithThumbprint(newCert); + + iis.AddOrUpdateBindings(new[] { host, newHost }, bindingOptions, null); + Assert.AreEqual(2, iis.WebSites.First().Bindings.Count()); + } } }
\ No newline at end of file diff --git a/src/main.test/Tests/BindingTests/HelperPerformance.cs b/src/main.test/Tests/BindingTests/HelperPerformance.cs new file mode 100644 index 0000000..613c942 --- /dev/null +++ b/src/main.test/Tests/BindingTests/HelperPerformance.cs @@ -0,0 +1,57 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PKISharp.WACS.Clients.IIS; +using PKISharp.WACS.DomainObjects; +using PKISharp.WACS.Services; +using PKISharp.WACS.UnitTests.Mock.Clients; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace PKISharp.WACS.UnitTests.Tests.BindingTests +{ + [TestClass] + public class HelperPerformance + { + private readonly ILogService log; + public HelperPerformance() => log = new Mock.Services.LogService(false); + + [TestMethod] + public void Speed() + { + var iis = new MockIISClient(log, 10); + var siteList = new List<MockSite>(); + for (var i = 0; i < 5000; i++) + { + var bindingList = new List<MockBinding>(); + for (var j = 0; j < 10; j++) + { + var randomBindingOptions = new BindingOptions(); + var randomId = ShortGuid.NewGuid().ToString(); + randomBindingOptions = randomBindingOptions.WithHost(randomId.ToLower()); + bindingList.Add(new MockBinding(randomBindingOptions)); + }; + siteList.Add(new MockSite() + { + Id = i, + Bindings = bindingList + }); + } + iis.MockSites = siteList.ToArray(); + var helper = new IISHelper(log, iis); + var timer = new Stopwatch(); + timer.Start(); + helper.GetSites(false); + timer.Stop(); + Assert.IsTrue(timer.ElapsedMilliseconds < 1000); + + timer.Reset(); + timer.Start(); + helper.GetBindings(); + timer.Stop(); + Assert.IsTrue(timer.ElapsedMilliseconds < 1000); + } + + + } +} diff --git a/src/main.test/wacs.test.csproj b/src/main.test/wacs.test.csproj index f691a52..bbbb8b4 100644 --- a/src/main.test/wacs.test.csproj +++ b/src/main.test/wacs.test.csproj @@ -9,7 +9,11 @@ <RootNamespace>PKISharp.WACS.UnitTests</RootNamespace> </PropertyGroup> - + + <PropertyGroup> + <Nullable>enable</Nullable> + </PropertyGroup> + <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> <PackageReference Include="MSTest.TestAdapter" Version="2.0.0" /> |