summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWouter Tinus <win.acme.simple@gmail.com>2020-03-01 13:22:35 +0100
committerGitHub <noreply@github.com>2020-03-01 13:22:35 +0100
commitc76a096f30c702aee7cadfd179421641f2a5fe4a (patch)
tree45fd536f35f098430985498d8316ad0a2c94c85b
parent2c3e6d331e2a071e34103b28dbaaf6ea7c767b36 (diff)
parent6acf0d45482324a919edf9404776736dfc782ed5 (diff)
downloadletsencrypt-win-simple-2.1.5.zip
letsencrypt-win-simple-2.1.5.tar.gz
letsencrypt-win-simple-2.1.5.tar.bz2
Merge pull request #1429 from win-acme/2.1.5v2.1.5
2.1.5
-rw-r--r--.gitmodules4
-rw-r--r--src/main.lib/Clients/Acme/AcmeClient.cs25
-rw-r--r--src/main.lib/Clients/IIS/IISClient.cs117
-rw-r--r--src/main.lib/Clients/IIS/IISHelper.cs17
-rw-r--r--src/main.lib/Clients/IIS/IISHttpBindingUpdater.cs42
-rw-r--r--src/main.lib/Clients/IIS/IISSiteWrapper.cs2
-rw-r--r--src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtp.cs7
-rw-r--r--src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWeb.cs5
-rw-r--r--src/main.lib/Plugins/Resolvers/InteractiveResolver.cs18
-rw-r--r--src/main.lib/Plugins/Resolvers/UnattendedResolver.cs31
-rw-r--r--src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs18
-rw-r--r--src/main.lib/Plugins/TargetPlugins/IIS/IIS.cs5
-rw-r--r--src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISBindingOptions.cs40
-rw-r--r--src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISSiteOptions.cs29
-rw-r--r--src/main.lib/Plugins/TargetPlugins/IIS/Legacy/IISSitesOptions.cs28
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs2
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs25
-rw-r--r--src/main.lib/RenewalCreator.cs5
-rw-r--r--src/main.lib/RenewalExecutor.cs225
-rw-r--r--src/main.lib/RenewalManager.cs208
-rw-r--r--src/main.lib/Services/ArgumentsService.cs13
-rw-r--r--src/main.lib/Services/CertificateService.cs6
-rw-r--r--src/main.lib/Services/InputService.cs7
-rw-r--r--src/main.lib/Services/Interfaces/IInputService.cs13
-rw-r--r--src/main.lib/Services/LogService.cs11
-rw-r--r--src/main.lib/Services/PluginService.cs2
-rw-r--r--src/main.lib/Services/ProxyService.cs5
-rw-r--r--src/main.lib/Services/RenewalStoreDisk.cs21
-rw-r--r--src/main.lib/Services/SettingsService.cs32
-rw-r--r--src/main.lib/Wacs.cs29
-rw-r--r--src/main.test/Mock/Clients/IISClient.cs17
-rw-r--r--src/main.test/Mock/Services/InputService.cs26
-rw-r--r--src/main.test/Tests/BindingTests/Bindings.cs51
-rw-r--r--src/main.test/Tests/BindingTests/HelperPerformance.cs57
-rw-r--r--src/main.test/wacs.test.csproj6
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" />