summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWouter Tinus <win.acme.simple@gmail.com>2020-07-10 13:22:30 +0200
committerGitHub <noreply@github.com>2020-07-10 13:22:30 +0200
commit594d90be29ec6090668966ad2fe2c9c9d46cace7 (patch)
treeb34d975bd0ae3aff1e46843e41a6bc7fe892fdc7
parente791885b06499c2cdd689bd7a7ed77fa8834a0cf (diff)
parentb2cea77b69fb05710f5f20f388878e92e213499d (diff)
downloadletsencrypt-win-simple-594d90be29ec6090668966ad2fe2c9c9d46cace7.zip
letsencrypt-win-simple-594d90be29ec6090668966ad2fe2c9c9d46cace7.tar.gz
letsencrypt-win-simple-594d90be29ec6090668966ad2fe2c9c9d46cace7.tar.bz2
Merge pull request #1609 from win-acme/2.1.9v2.1.9
2.1.9
-rw-r--r--appveyor.yml2
m---------src/ACMESharpCore0
-rw-r--r--src/main.lib/Clients/Acme/AcmeClient.cs18
-rw-r--r--src/main.lib/Clients/EmailClient.cs20
-rw-r--r--src/main.lib/Clients/IIS/IISClient.cs9
-rw-r--r--src/main.lib/Context/ExecutionContext.cs26
-rw-r--r--src/main.lib/Context/ValidationContext.cs77
-rw-r--r--src/main.lib/Plugins/Interfaces/IValidationPlugin.cs28
-rw-r--r--src/main.lib/Plugins/Resolvers/InteractiveResolver.cs22
-rw-r--r--src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs2
-rw-r--r--src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptionsFactory.cs2
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Dns/Acme/Acme.cs12
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs182
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Dns/Manual/Manual.cs28
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Dns/Script/Script.cs53
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs18
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs8
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs206
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationParameters.cs8
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHosting.cs85
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs9
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDav.cs12
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Tls/SelfHosting/SelfHosting.cs21
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Validation.cs35
-rw-r--r--src/main.lib/RenewalCreator.cs4
-rw-r--r--src/main.lib/RenewalExecutor.cs229
-rw-r--r--src/main.lib/RenewalManager.cs10
-rw-r--r--src/main.lib/RenewalValidator.cs447
-rw-r--r--src/main.lib/Services/AutofacBuilder.cs11
-rw-r--r--src/main.lib/Services/Interfaces/IAutofacBuilder.cs2
-rw-r--r--src/main.lib/Services/NotificationService.cs13
-rw-r--r--src/main.lib/Services/PluginService.cs4
-rw-r--r--src/main.lib/Services/SettingsService.cs5
-rw-r--r--src/main.lib/Services/TaskSchedulerService.cs6
-rw-r--r--src/main.test/Mock/MockContainer.cs1
-rw-r--r--src/main.test/Tests/BindingTests/Bindings.cs35
-rw-r--r--src/main.test/Tests/RenewalTests/RenewalServiceTests.cs5
-rw-r--r--src/main/Program.cs1
-rw-r--r--src/main/settings.json3
-rwxr-xr-xsrc/plugin.validation.dns.azure/Azure.cs193
-rw-r--r--src/plugin.validation.dns.cloudflare/Cloudflare.cs16
-rw-r--r--src/plugin.validation.dns.dreamhost/DreamhostDnsValidation.cs9
-rw-r--r--src/plugin.validation.dns.luadns/luadns.cs20
-rw-r--r--src/plugin.validation.dns.route53/Route53.cs64
44 files changed, 1318 insertions, 643 deletions
diff --git a/appveyor.yml b/appveyor.yml
index 0af7350..ff2cc11 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 2.1.8.{build}
+version: 2.1.9.{build}
image: Visual Studio 2019
platform: Any CPU
shallow_clone: true
diff --git a/src/ACMESharpCore b/src/ACMESharpCore
-Subproject 2e6dcd3f01696c5ed18483f53c670031f0d81c0
+Subproject bd6f0bbc0b0e1cb17303324a2a0b4c657a045ed
diff --git a/src/main.lib/Clients/Acme/AcmeClient.cs b/src/main.lib/Clients/Acme/AcmeClient.cs
index 2783881..864ab7d 100644
--- a/src/main.lib/Clients/Acme/AcmeClient.cs
+++ b/src/main.lib/Clients/Acme/AcmeClient.cs
@@ -16,6 +16,7 @@ using System.Net.Http;
using System.Net.Mail;
using System.Security.Authentication;
using System.Security.Cryptography;
+using System.Threading;
using System.Threading.Tasks;
namespace PKISharp.WACS.Clients.Acme
@@ -529,6 +530,10 @@ namespace PKISharp.WACS.Clients.Acme
/// <returns></returns>
private async Task<T> Retry<T>(Func<Task<T>> executor, int attempt = 0)
{
+ if (attempt == 0)
+ {
+ await _requestLock.WaitAsync();
+ }
try
{
return await executor();
@@ -547,8 +552,21 @@ namespace PKISharp.WACS.Clients.Acme
throw;
}
}
+ finally
+ {
+ if (attempt == 0)
+ {
+ _requestLock.Release();
+ }
+ }
}
+ /// <summary>
+ /// Prevent sending simulateous requests to the ACME service because it messes
+ /// up the nonce tracking mechanism
+ /// </summary>
+ private readonly SemaphoreSlim _requestLock = new SemaphoreSlim(1, 1);
+
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
diff --git a/src/main.lib/Clients/EmailClient.cs b/src/main.lib/Clients/EmailClient.cs
index 5e78920..4f77115 100644
--- a/src/main.lib/Clients/EmailClient.cs
+++ b/src/main.lib/Clients/EmailClient.cs
@@ -66,13 +66,13 @@ namespace PKISharp.WACS.Clients
public bool Enabled { get; internal set; }
- public void Send(string subject, string content, MessagePriority priority)
+ public async Task Send(string subject, string content, MessagePriority priority)
{
if (Enabled)
{
+ using var client = new SmtpClient();
try
{
- using var client = new SmtpClient();
var options = SecureSocketOptions.None;
if (_secure)
{
@@ -99,10 +99,10 @@ namespace PKISharp.WACS.Clients
options = SecureSocketOptions.StartTls;
}
}
- client.Connect(_server, _port, options);
+ await client.ConnectAsync(_server, _port, options);
if (!string.IsNullOrEmpty(_user))
{
- client.Authenticate(new NetworkCredential(_user, _password));
+ await client.AuthenticateAsync(new NetworkCredential(_user, _password));
}
foreach (var receiverAddress in _receiverAddresses)
{
@@ -121,17 +121,22 @@ namespace PKISharp.WACS.Clients
var bodyBuilder = new BodyBuilder();
bodyBuilder.HtmlBody = content + $"<p>Sent by win-acme version {_version} from {_computerName}</p>";
message.Body = bodyBuilder.ToMessageBody();
- client.Send(message);
+ await client.SendAsync(message);
+ await client.DisconnectAsync(true);
}
}
catch (Exception ex)
{
_log.Error(ex, "Problem sending e-mail");
+ }
+ finally
+ {
+
}
}
}
- internal Task Test()
+ internal async Task Test()
{
if (!Enabled)
{
@@ -140,12 +145,11 @@ namespace PKISharp.WACS.Clients
else
{
_log.Information("Sending test message...");
- Send("Test notification",
+ await Send("Test notification",
"<p>If you are reading this, it means you will receive notifications about critical errors in the future.</p>",
MessagePriority.Normal);
_log.Information("Test message sent!");
}
- return Task.CompletedTask;
}
}
}
diff --git a/src/main.lib/Clients/IIS/IISClient.cs b/src/main.lib/Clients/IIS/IISClient.cs
index 6830086..3efa22a 100644
--- a/src/main.lib/Clients/IIS/IISClient.cs
+++ b/src/main.lib/Clients/IIS/IISClient.cs
@@ -40,7 +40,14 @@ namespace PKISharp.WACS.Clients.IIS
{
if (Version.Major > 0)
{
- _serverManager = new ServerManager();
+ try
+ {
+ _serverManager = new ServerManager();
+ }
+ catch
+ {
+ _log.Error($"Unable to create an IIS ServerManager");
+ }
_webSites = null;
_ftpSites = null;
}
diff --git a/src/main.lib/Context/ExecutionContext.cs b/src/main.lib/Context/ExecutionContext.cs
new file mode 100644
index 0000000..23d3373
--- /dev/null
+++ b/src/main.lib/Context/ExecutionContext.cs
@@ -0,0 +1,26 @@
+using Autofac;
+using PKISharp.WACS.DomainObjects;
+
+namespace PKISharp.WACS.Context
+{
+ /// <summary>
+ /// Common objects used throughout the renewal process
+ /// </summary>
+ internal class ExecutionContext
+ {
+ public ILifetimeScope Scope { get; private set; }
+ public Order Order { get; private set; }
+ public RunLevel RunLevel { get; private set; }
+ public RenewResult Result { get; private set; }
+ public Target Target => Order.Target;
+ public Renewal Renewal => Order.Renewal;
+
+ public ExecutionContext(ILifetimeScope scope, Order order, RunLevel runLevel, RenewResult result)
+ {
+ Scope = scope;
+ Order = order;
+ RunLevel = runLevel;
+ Result = result;
+ }
+ }
+}
diff --git a/src/main.lib/Context/ValidationContext.cs b/src/main.lib/Context/ValidationContext.cs
new file mode 100644
index 0000000..4dddb76
--- /dev/null
+++ b/src/main.lib/Context/ValidationContext.cs
@@ -0,0 +1,77 @@
+using ACMESharp.Authorizations;
+using ACMESharp.Protocol.Resources;
+using Autofac;
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Plugins.Interfaces;
+using System.Collections.Generic;
+
+namespace PKISharp.WACS.Context
+{
+ public class ValidationContextParameters
+ {
+ public ValidationContextParameters(
+ Authorization authorization,
+ TargetPart targetPart,
+ string challengeType,
+ string pluginName,
+ bool orderValid)
+ {
+ TargetPart = targetPart;
+ Authorization = authorization;
+ ChallengeType = challengeType;
+ PluginName = pluginName;
+ OrderValid = orderValid;
+ }
+ public bool OrderValid { get; }
+ public string ChallengeType { get; }
+ public string PluginName { get; }
+ public TargetPart TargetPart { get; }
+ public Authorization Authorization { get; }
+ }
+
+ public class ValidationContext
+ {
+ public ValidationContext(
+ ILifetimeScope scope,
+ ValidationContextParameters parameters)
+ {
+ Identifier = parameters.Authorization.Identifier.Value;
+ TargetPart = parameters.TargetPart;
+ Authorization = parameters.Authorization;
+ Scope = scope;
+ ChallengeType = parameters.ChallengeType;
+ PluginName = parameters.PluginName;
+ ValidationPlugin = scope.Resolve<IValidationPlugin>();
+ if (parameters.OrderValid)
+ {
+ Success = true;
+ }
+ }
+ public ILifetimeScope Scope { get; }
+ public string Identifier { get; }
+ public string ChallengeType { get; }
+ public string PluginName { get; }
+ public TargetPart? TargetPart { get; }
+ public Authorization Authorization { get; }
+ public Challenge? Challenge { get; set; }
+ public IChallengeValidationDetails? ChallengeDetails { get; set; }
+ public IValidationPlugin ValidationPlugin { get; set; }
+ public bool? Success { get; set; }
+ public List<string> ErrorMessages { get; } = new List<string>();
+ public void AddErrorMessage(string? value, bool fatal = true)
+ {
+ if (value != null)
+ {
+ if (!ErrorMessages.Contains(value))
+ {
+ ErrorMessages.Add(value);
+ }
+ }
+ if (fatal)
+ {
+ Success = false;
+ }
+ }
+ }
+
+}
diff --git a/src/main.lib/Plugins/Interfaces/IValidationPlugin.cs b/src/main.lib/Plugins/Interfaces/IValidationPlugin.cs
index ba899db..a7c8249 100644
--- a/src/main.lib/Plugins/Interfaces/IValidationPlugin.cs
+++ b/src/main.lib/Plugins/Interfaces/IValidationPlugin.cs
@@ -1,4 +1,4 @@
-using ACMESharp.Authorizations;
+using PKISharp.WACS.Context;
using System;
using System.Threading.Tasks;
@@ -10,17 +10,37 @@ namespace PKISharp.WACS.Plugins.Interfaces
public interface IValidationPlugin : IPlugin
{
/// <summary>
- /// Prepare challenge
+ /// Prepare single challenge
/// </summary>
/// <param name="options"></param>
/// <param name="target"></param>
/// <param name="challenge"></param>
/// <returns></returns>
- Task PrepareChallenge(IChallengeValidationDetails challengeDetails);
+ Task PrepareChallenge(ValidationContext context);
+
+ /// <summary>
+ /// Commit changes after all the challenges have been prepared
+ /// </summary>
+ /// <returns></returns>
+ Task Commit();
/// <summary>
/// Clean up after validation attempt
/// </summary>
- Task CleanUp();
+ Task CleanUp();
+
+ /// <summary>
+ /// Indicate level of supported parallelism
+ /// </summary>
+ ParallelOperations Parallelism { get; }
+ }
+
+ [Flags]
+ public enum ParallelOperations
+ {
+ None = 0,
+ Prepare = 1,
+ Answer = 2,
+ Clean = 4
}
}
diff --git a/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs b/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs
index 257e88d..dc96981 100644
--- a/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs
+++ b/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs
@@ -185,6 +185,16 @@ namespace PKISharp.WACS.Plugins.Resolvers
/// <returns></returns>
public override async Task<IValidationPluginOptionsFactory> GetValidationPlugin(ILifetimeScope scope, Target target)
{
+ var defaultParam1 = _settings.Validation.DefaultValidation;
+ var defaultParam2 = _settings.Validation.DefaultValidationMode ?? Constants.Http01ChallengeType;
+ if (!string.IsNullOrWhiteSpace(_arguments.MainArguments.Validation))
+ {
+ defaultParam1 = _arguments.MainArguments.Validation;
+ }
+ if (!string.IsNullOrWhiteSpace(_arguments.MainArguments.ValidationMode))
+ {
+ defaultParam2 = _arguments.MainArguments.ValidationMode;
+ }
return await GetPlugin<IValidationPluginOptionsFactory>(
scope,
sort: x =>
@@ -203,8 +213,8 @@ namespace PKISharp.WACS.Plugins.Resolvers
ThenBy(x => x.Description),
unusable: x => (!x.CanValidate(target), "Unsuppored target. Most likely this is because you have included a wildcard identifier (*.example.com), which requires DNS validation."),
description: x => $"[{x.ChallengeType}] {x.Description}",
- defaultParam1: _settings.Validation.DefaultValidation,
- defaultParam2: _settings.Validation.DefaultValidationMode ?? Constants.Http01ChallengeType,
+ defaultParam1: defaultParam1,
+ defaultParam2: defaultParam2,
defaultType: typeof(SelfHostingOptionsFactory),
defaultTypeFallback: typeof(FileSystemOptionsFactory),
nullResult: new NullValidationFactory(),
@@ -250,6 +260,10 @@ namespace PKISharp.WACS.Plugins.Resolvers
defaultType = typeof(NullStoreOptionsFactory);
}
var defaultParam1 = _settings.Store.DefaultStore;
+ if (!string.IsNullOrWhiteSpace(_arguments.MainArguments.Store))
+ {
+ defaultParam1 = _arguments.MainArguments.Store;
+ }
var csv = defaultParam1.ParseCsv();
defaultParam1 = csv?.Count > chosen.Count() ?
csv[chosen.Count()] :
@@ -292,6 +306,10 @@ namespace PKISharp.WACS.Plugins.Resolvers
defaultType = typeof(NullInstallationOptionsFactory);
}
var defaultParam1 = _settings.Installation.DefaultInstallation;
+ if (!string.IsNullOrWhiteSpace(_arguments.MainArguments.Installation))
+ {
+ defaultParam1 = _arguments.MainArguments.Installation;
+ }
var csv = defaultParam1.ParseCsv();
defaultParam1 = csv?.Count > chosen.Count() ?
csv[chosen.Count()] :
diff --git a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs
index 6da551c..a9e3a48 100644
--- a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs
+++ b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs
@@ -19,7 +19,7 @@ namespace PKISharp.WACS.Plugins.StorePlugins
public static string? DefaultPath(ISettingsService settings)
{
- var ret = settings.Store.PemFiles?.DefaultPath;
+ var ret = settings.Store.CentralSsl?.DefaultPath;
if (string.IsNullOrWhiteSpace(ret))
{
ret = settings.Store.DefaultCentralSslStore;
diff --git a/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptionsFactory.cs b/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptionsFactory.cs
index 8cc998e..00359b4 100644
--- a/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptionsFactory.cs
+++ b/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptionsFactory.cs
@@ -28,7 +28,7 @@ namespace PKISharp.WACS.Plugins.StorePlugins
var path = args?.PfxFilePath;
if (string.IsNullOrWhiteSpace(path))
{
- path = _settings.Store.PfxFile?.DefaultPassword;
+ path = _settings.Store.PfxFile?.DefaultPath;
}
while (string.IsNullOrWhiteSpace(path) || !path.ValidPath(_log))
{
diff --git a/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/Acme.cs b/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/Acme.cs
index ddcff59..e89e289 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/Acme.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/Acme.cs
@@ -1,5 +1,6 @@
using PKISharp.WACS.Clients;
using PKISharp.WACS.Clients.DNS;
+using PKISharp.WACS.Context;
using PKISharp.WACS.Services;
using System;
using System.Threading.Tasks;
@@ -11,7 +12,6 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
private readonly IInputService _input;
private readonly ProxyService _proxy;
private readonly AcmeOptions _options;
- private readonly string _identifier;
public Acme(
LookupClientProvider dnsClient,
@@ -19,12 +19,10 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
ISettingsService settings,
IInputService input,
ProxyService proxy,
- AcmeOptions options,
- string identifier) :
+ AcmeOptions options) :
base(dnsClient, log, settings)
{
_options = options;
- _identifier = identifier;
_input = input;
_proxy = proxy;
}
@@ -34,12 +32,12 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
/// </summary>
/// <param name="recordName"></param>
/// <param name="token"></param>
- public override async Task<bool> CreateRecord(string recordName, string token)
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
{
var client = new AcmeDnsClient(_dnsClient, _proxy, _log, _settings, _input, new Uri(_options.BaseUri));
- return await client.Update(_identifier, token);
+ return await client.Update(record.Context.Identifier, record.Value);
}
- public override Task DeleteRecord(string recordName, string token) => Task.CompletedTask;
+ public override Task DeleteRecord(DnsValidationRecord record) => Task.CompletedTask;
}
}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs b/src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs
index 87e5273..0c39ec7 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs
@@ -1,10 +1,10 @@
using ACMESharp.Authorizations;
using PKISharp.WACS.Clients.DNS;
+using PKISharp.WACS.Context;
using PKISharp.WACS.Services;
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Threading;
using System.Threading.Tasks;
using static PKISharp.WACS.Clients.DNS.LookupClientProvider;
@@ -18,10 +18,10 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
protected readonly LookupClientProvider _dnsClient;
protected readonly ILogService _log;
protected readonly ISettingsService _settings;
- private DnsLookupResult? _authority;
+ private readonly List<DnsValidationRecord> _recordsCreated = new List<DnsValidationRecord>();
protected DnsValidation(
- LookupClientProvider dnsClient,
+ LookupClientProvider dnsClient,
ILogService log,
ISettingsService settings)
{
@@ -30,124 +30,179 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
_settings = settings;
}
- public override async Task PrepareChallenge()
+ /// <summary>
+ /// Prepare to add a new DNS record
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="challenge"></param>
+ /// <returns></returns>
+ public override async Task PrepareChallenge(ValidationContext context, Dns01ChallengeValidationDetails challenge)
{
// Check for substitute domains
- _authority = await _dnsClient.GetAuthority(
- Challenge.DnsRecordName,
+ var authority = await _dnsClient.GetAuthority(
+ challenge.DnsRecordName,
followCnames: _settings.Validation.AllowDnsSubstitution);
var success = false;
- while (!success)
+ while (!success)
{
- success = await CreateRecord(_authority.Domain, Challenge.DnsRecordValue);
+ var record = new DnsValidationRecord(context, authority, challenge.DnsRecordValue);
+ success = await CreateRecord(record);
if (!success)
{
- if (_authority.From == null)
+ if (authority.From == null)
{
throw new Exception("Unable to prepare for challenge answer");
}
else
{
- _authority = _authority.From;
+ authority = authority.From;
}
+ }
+ else
+ {
+ _recordsCreated.Add(record);
}
- }
+ }
+ }
+
+ /// <summary>
+ /// Default commit function, doesn't do anything because
+ /// default doesn't do parallel operation
+ /// </summary>
+ /// <returns></returns>
+ public override sealed async Task Commit()
+ {
+ // Wait for changes to be saved
+ await SaveChanges();
// Verify that the record was created succesfully and wait for possible
// propagation/caching/TTL issues to resolve themselves naturally
- var retry = 0;
- var maxRetries = _settings.Validation.PreValidateDnsRetryCount;
- var retrySeconds = _settings.Validation.PreValidateDnsRetryInterval;
- while (_settings.Validation.PreValidateDns)
+ if (_settings.Validation.PreValidateDns)
{
- if (await PreValidate())
- {
- break;
- }
- else
- {
- retry += 1;
- if (retry > maxRetries)
- {
- _log.Information("It looks like validation is going to fail, but we will try now anyway...");
- break;
- }
- else
- {
- _log.Information("Will retry in {s} seconds (retry {i}/{j})...", retrySeconds, retry, maxRetries);
- Thread.Sleep(retrySeconds * 1000);
- }
- }
+ var validationTasks = _recordsCreated.Select(r => ValidateRecord(r));
+ await Task.WhenAll(validationTasks);
}
}
- protected async Task<bool> PreValidate()
+ /// <summary>
+ /// Typically the changes will already be saved by
+ /// PrepareChallenge, but for those plugins that support
+ /// parallel operation, this may be overridden to handle
+ /// persistance
+ /// </summary>
+ /// <returns></returns>
+ public virtual Task SaveChanges() => Task.CompletedTask;
+
+ /// <summary>
+ /// Check the TXT value from all known authoritative DNS servers
+ /// </summary>
+ /// <param name="record"></param>
+ /// <returns></returns>
+ protected async Task<bool> PreValidate(DnsValidationRecord record)
{
try
{
- if (_authority == null)
- {
- throw new InvalidOperationException("_recordName is null");
- }
- _log.Debug("Looking for TXT value {DnsRecordValue}...", _authority.Domain);
- foreach (var client in _authority.Nameservers)
+ _log.Debug("[{identifier}] Looking for TXT value {DnsRecordValue}...", record.Context.Identifier, record.Authority.Domain);
+ foreach (var client in record.Authority.Nameservers)
{
- _log.Debug("Preliminary validation asking {ip}...", client.IpAddress);
- var answers = await client.GetTxtRecords(_authority.Domain);
+ _log.Debug("[{identifier}] Preliminary validation asking {ip}...", record.Context.Identifier, client.IpAddress);
+ var answers = await client.GetTxtRecords(record.Authority.Domain);
if (!answers.Any())
{
- _log.Warning("Preliminary validation failed: no TXT records found");
+ _log.Warning("[{identifier}] Preliminary validation failed: no TXT records found", record.Context.Identifier);
return false;
}
- if (!answers.Contains(Challenge.DnsRecordValue))
+ if (!answers.Contains(record.Value))
{
- _log.Debug("Preliminary validation found values: {answers}", answers);
- _log.Warning("Preliminary validation failed: incorrect TXT record(s) found");
+ _log.Debug("[{identifier}] Preliminary validation found values: {answers}", record.Context.Identifier, answers);
+ _log.Warning("[{identifier}] Preliminary validation failed: incorrect TXT record(s) found", record.Context.Identifier);
return false;
}
- _log.Debug("Preliminary validation from {ip} looks good", client.IpAddress);
+ _log.Debug("[{identifier}] Preliminary validation from {ip} looks good", record.Context.Identifier, client.IpAddress);
}
}
catch (Exception ex)
{
- _log.Error(ex, "Preliminary validation failed");
+ _log.Error(ex, "[{identifier}] Preliminary validation failed", record.Context.Identifier);
return false;
}
- _log.Information("Preliminary validation succeeded");
+ _log.Information("[{identifier}] Preliminary validation succeeded", record.Context.Identifier);
return true;
}
/// <summary>
/// Delete record when we're done
/// </summary>
- public override async Task CleanUp()
+ public override sealed async Task CleanUp()
{
- if (HasChallenge && _authority != null)
+ foreach (var record in _recordsCreated)
{
try
{
- await DeleteRecord(_authority.Domain, Challenge.DnsRecordValue);
- }
+ await DeleteRecord(record);
+ }
catch (Exception ex)
{
_log.Warning($"Error deleting record: {ex.Message}");
}
}
+ await Finalize();
+ }
+
+ /// <summary>
+ /// Typically the changes will already be undone by
+ /// Finalize, but for those plugins that support
+ /// parallel operation, this may be overridden
+ /// </summary>
+ /// <returns></returns>
+ public virtual Task Finalize() => Task.CompletedTask;
+
+ /// <summary>
+ /// Validate a record as being correctly created an sychronised, runs during/after the commit state
+ /// </summary>
+ /// <param name="record"></param>
+ /// <returns></returns>
+ private async Task ValidateRecord(DnsValidationRecord record)
+ {
+ var retry = 0;
+ var maxRetries = _settings.Validation.PreValidateDnsRetryCount;
+ var retrySeconds = _settings.Validation.PreValidateDnsRetryInterval;
+ while (true)
+ {
+ if (await PreValidate(record))
+ {
+ break;
+ }
+ else
+ {
+ retry += 1;
+ if (retry > maxRetries)
+ {
+ _log.Information("It looks like validation is going to fail, but we will try now anyway...");
+ break;
+ }
+ else
+ {
+ _log.Information("Will retry in {s} seconds (retry {i}/{j})...", retrySeconds, retry, maxRetries);
+ await Task.Delay(retrySeconds * 1000);
+ }
+ }
+ }
}
/// <summary>
/// Delete validation record
/// </summary>
/// <param name="recordName">Name of the record</param>
- public abstract Task DeleteRecord(string recordName, string token);
+ public abstract Task DeleteRecord(DnsValidationRecord record);
/// <summary>
/// Create validation record
/// </summary>
/// <param name="recordName">Name of the record</param>
/// <param name="token">Contents of the record</param>
- public abstract Task<bool> CreateRecord(string recordName, string token);
+ public abstract Task<bool> CreateRecord(DnsValidationRecord record);
/// <summary>
/// Match DNS zone to use from a list of all zones
@@ -196,5 +251,22 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
return null;
}
}
+
+ /// <summary>
+ /// Keep track of which records are created, so that they can be deleted later
+ /// </summary>
+ public class DnsValidationRecord
+ {
+ public ValidationContext Context { get; }
+ public DnsLookupResult Authority { get; }
+ public string Value { get; }
+
+ public DnsValidationRecord(ValidationContext context, DnsLookupResult authority, string value)
+ {
+ Context = context;
+ Authority = authority;
+ Value = value;
+ }
+ }
}
}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Dns/Manual/Manual.cs b/src/main.lib/Plugins/ValidationPlugins/Dns/Manual/Manual.cs
index c584efb..880eb6c 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Dns/Manual/Manual.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Dns/Manual/Manual.cs
@@ -1,4 +1,6 @@
using PKISharp.WACS.Clients.DNS;
+using PKISharp.WACS.Context;
+using PKISharp.WACS.Plugins.Interfaces;
using PKISharp.WACS.Services;
using System.Threading.Tasks;
@@ -7,31 +9,29 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
internal class Manual : DnsValidation<Manual>
{
private readonly IInputService _input;
- private readonly string _identifier;
+
+ public override ParallelOperations Parallelism => ParallelOperations.Answer;
public Manual(
LookupClientProvider dnsClient,
ILogService log,
IInputService input,
- ISettingsService settings,
- string identifier)
- : base(dnsClient, log, settings)
+ ISettingsService settings) : base(dnsClient, log, settings)
{
// Usually it's a big no-no to rely on user input in validation plugin
// because this should be able to run unattended. This plugin is for testing
// only and therefor we will allow it. Future versions might be more advanced,
// e.g. shoot an email to an admin and complete the order later.
_input = input;
- _identifier = identifier;
}
- public override async Task<bool> CreateRecord(string recordName, string token)
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
{
_input.CreateSpace();
- _input.Show("Domain", _identifier);
- _input.Show("Record", recordName);
+ _input.Show("Domain", record.Context.Identifier);
+ _input.Show("Record", record.Authority.Domain);
_input.Show("Type", "TXT");
- _input.Show("Content", $"\"{token}\"");
+ _input.Show("Content", $"\"{record.Value}\"");
_input.Show("Note", "Some DNS managers add quotes automatically. A single set is needed.");
if (!await _input.Wait("Please press <Enter> after you've created and verified the record"))
{
@@ -42,7 +42,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
// Pre-pre-validate, allowing the manual user to correct mistakes
while (true)
{
- if (await PreValidate())
+ if (await PreValidate(record))
{
return true;
}
@@ -61,13 +61,13 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
}
}
- public override Task DeleteRecord(string recordName, string token)
+ public override Task DeleteRecord(DnsValidationRecord record)
{
_input.CreateSpace();
- _input.Show("Domain", _identifier);
- _input.Show("Record", recordName);
+ _input.Show("Domain", record.Context.Identifier);
+ _input.Show("Record", record.Authority.Domain);
_input.Show("Type", "TXT");
- _input.Show("Content", $"\"{token}\"");
+ _input.Show("Content", $"\"{record.Value}\"");
_input.Wait("Please press <Enter> after you've deleted the record");
return Task.CompletedTask;
}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Dns/Script/Script.cs b/src/main.lib/Plugins/ValidationPlugins/Dns/Script/Script.cs
index 80ece5e..222a7d2 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Dns/Script/Script.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Dns/Script/Script.cs
@@ -9,8 +9,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
{
private readonly ScriptClient _scriptClient;
private readonly ScriptOptions _options;
- private readonly string _identifier;
-
+ private readonly DomainParseService _domainParseService;
internal const string DefaultCreateArguments = "create {Identifier} {RecordName} {Token}";
internal const string DefaultDeleteArguments = "delete {Identifier} {RecordName} {Token}";
@@ -19,16 +18,16 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
LookupClientProvider dnsClient,
ScriptClient client,
ILogService log,
- ISettingsService settings,
- string identifier) :
+ DomainParseService domainParseService,
+ ISettingsService settings) :
base(dnsClient, log, settings)
{
- _identifier = identifier;
_options = options;
_scriptClient = client;
+ _domainParseService = domainParseService;
}
- public override async Task<bool> CreateRecord(string recordName, string token)
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
{
var script = _options.Script ?? _options.CreateScript;
if (!string.IsNullOrWhiteSpace(script))
@@ -38,7 +37,14 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
{
args = _options.CreateScriptArguments;
}
- await _scriptClient.RunScript(script, ProcessArguments(recordName, token, args, script.EndsWith(".ps1")));
+ await _scriptClient.RunScript(
+ script,
+ ProcessArguments(
+ record.Context.Identifier,
+ record.Authority.Domain,
+ record.Value,
+ args,
+ script.EndsWith(".ps1")));
return true;
}
else
@@ -48,7 +54,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
}
}
- public override async Task DeleteRecord(string recordName, string token)
+ public override async Task DeleteRecord(DnsValidationRecord record)
{
var script = _options.Script ?? _options.DeleteScript;
if (!string.IsNullOrWhiteSpace(script))
@@ -58,7 +64,14 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
{
args = _options.DeleteScriptArguments;
}
- await _scriptClient.RunScript(script, ProcessArguments(recordName, token, args, script.EndsWith(".ps1")));
+ await _scriptClient.RunScript(
+ script,
+ ProcessArguments(
+ record.Context.Identifier,
+ record.Authority.Domain,
+ record.Value,
+ args,
+ script.EndsWith(".ps1")));
}
else
{
@@ -66,11 +79,29 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
}
}
- private string ProcessArguments(string recordName, string token, string args, bool escapeToken)
+ private string ProcessArguments(string identifier, string recordName, string token, string args, bool escapeToken)
{
var ret = args;
- ret = ret.Replace("{Identifier}", _identifier);
+ // recordName: _acme-challenge.sub.domain.com
+ // zoneName: domain.com
+ // nodeName: _acme-challenge.sub
+
+ // recordName: domain.com
+ // zoneName: domain.com
+ // nodeName: @
+
+ var zoneName = _domainParseService.GetRegisterableDomain(identifier);
+ var nodeName = "@";
+ if (recordName != zoneName)
+ {
+ // Offset by one to prevent trailing dot
+ nodeName = recordName.Substring(0, recordName.Length - zoneName.Length - 1);
+ }
+ ret = ret.Replace("{ZoneName}", zoneName);
+ ret = ret.Replace("{NodeName}", nodeName);
+ ret = ret.Replace("{Identifier}", identifier);
ret = ret.Replace("{RecordName}", recordName);
+
// Some tokens start with - which confuses Powershell. We did not want to
// make a breaking change for .bat or .exe files, so instead escape the
// token with double quotes, as Powershell discards the quotes anyway and
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs
index f8d5b0b..8dfccb4 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs
@@ -1,7 +1,9 @@
using PKISharp.WACS.Clients.IIS;
+using PKISharp.WACS.DomainObjects;
using System;
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
{
@@ -11,7 +13,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
public FileSystem(FileSystemOptions options, IIISClient iisClient, RunLevel runLevel, HttpValidationParameters pars) : base(options, runLevel, pars) => _iisClient = iisClient;
- protected override void DeleteFile(string path)
+ protected override Task DeleteFile(string path)
{
var fi = new FileInfo(path);
if (fi.Exists)
@@ -23,9 +25,10 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
{
_log.Warning("File {path} already deleted", path);
}
+ return Task.CompletedTask;
}
- protected override void DeleteFolder(string path)
+ protected override Task DeleteFolder(string path)
{
var di = new DirectoryInfo(path);
if (di.Exists)
@@ -37,11 +40,12 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
{
_log.Warning("Folder {path} already deleted", path);
}
+ return Task.CompletedTask;
}
- protected override bool IsEmpty(string path) => !new DirectoryInfo(path).EnumerateFileSystemInfos().Any();
+ protected override Task<bool> IsEmpty(string path) => Task.FromResult(!new DirectoryInfo(path).EnumerateFileSystemInfos().Any());
- protected override void WriteFile(string path, string content)
+ protected override async Task WriteFile(string path, string content)
{
var fi = new FileInfo(path);
if (!fi.Directory.Exists)
@@ -49,19 +53,19 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
fi.Directory.Create();
}
_log.Verbose("Writing file to {path}", path);
- File.WriteAllText(path, content);
+ await File.WriteAllTextAsync(path, content);
}
/// <summary>
/// Update webroot
/// </summary>
/// <param name="scheduled"></param>
- protected override void Refresh()
+ protected override void Refresh(TargetPart targetPart)
{
if (string.IsNullOrEmpty(_options.Path))
{
// Update web root path
- var siteId = _options.SiteId ?? _targetPart.SiteId;
+ var siteId = _options.SiteId ?? targetPart.SiteId;
if (siteId > 0)
{
_path = _iisClient.GetWebSite(siteId.Value).Path;
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs b/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs
index 6dbde44..c9aa07e 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs
@@ -12,12 +12,12 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
protected override char PathSeparator => '/';
- protected override void DeleteFile(string path) => _ftpClient.Delete(path, FtpClient.FileType.File);
+ protected override async Task DeleteFile(string path) => _ftpClient.Delete(path, FtpClient.FileType.File);
- protected override void DeleteFolder(string path) => _ftpClient.Delete(path, FtpClient.FileType.Directory);
+ protected override async Task DeleteFolder(string path) => _ftpClient.Delete(path, FtpClient.FileType.Directory);
- protected override bool IsEmpty(string path) => !_ftpClient.GetFiles(path).Any();
+ protected override async Task<bool> IsEmpty(string path) => !_ftpClient.GetFiles(path).Any();
- protected override void WriteFile(string path, string content) => _ftpClient.Upload(path, content);
+ protected override async Task WriteFile(string path, string content) => _ftpClient.Upload(path, content);
}
}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs
index bc811fe..ee6a996 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs
@@ -1,8 +1,10 @@
using ACMESharp.Authorizations;
+using PKISharp.WACS.Context;
using PKISharp.WACS.DomainObjects;
using PKISharp.WACS.Plugins.Interfaces;
using PKISharp.WACS.Services;
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
@@ -19,9 +21,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
where TOptions : HttpValidationOptions<TPlugin>
where TPlugin : IValidationPlugin
{
-
- private bool _webConfigWritten = false;
- private bool _challengeWritten = false;
+ private readonly List<string> _filesWritten = new List<string>();
protected TOptions _options;
protected ILogService _log;
@@ -31,6 +31,11 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
protected RunLevel _runLevel;
/// <summary>
+ /// Multiple http-01 validation challenges can be answered at the same time
+ /// </summary>
+ public override ParallelOperations Parallelism => ParallelOperations.Answer;
+
+ /// <summary>
/// Path used for the current renewal, may not be same as _options.Path
/// because of the "Split" function employed by IISSites target
/// </summary>
@@ -42,13 +47,6 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
private readonly ProxyService _proxy;
/// <summary>
- /// Current TargetPart that we are working on. A TargetPart is mainly used by
- /// the IISSites TargetPlugin to indicate that we are working with different
- /// IIS sites
- /// </summary>
- protected TargetPart _targetPart;
-
- /// <summary>
/// Where to find the template for the web.config that's copied to the webroot
/// </summary>
protected string TemplateWebConfig => Path.Combine(Path.GetDirectoryName(_settings.ExePath), "web_config.xml");
@@ -79,25 +77,29 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
_proxy = pars.ProxyService;
_settings = pars.Settings;
_renewal = pars.Renewal;
- _targetPart = pars.TargetPart;
}
/// <summary>
/// Handle http challenge
/// </summary>
- public async override Task PrepareChallenge()
+ public async override Task PrepareChallenge(ValidationContext context, Http01ChallengeValidationDetails challenge)
{
- Refresh();
- WriteAuthorizationFile();
- WriteWebConfig();
- _log.Information("Answer should now be browsable at {answerUri}", Challenge.HttpResourceUrl);
+ // Should always have a value, confirmed by RenewalExecutor
+ // check only to satifiy the compiler
+ if (context.TargetPart != null)
+ {
+ Refresh(context.TargetPart);
+ }
+ WriteAuthorizationFile(challenge);
+ WriteWebConfig(challenge);
+ _log.Information("Answer should now be browsable at {answerUri}", challenge.HttpResourceUrl);
if (_runLevel.HasFlag(RunLevel.Test) && _renewal.New)
{
if (await _input.PromptYesNo("[--test] Try in default browser?", false))
{
Process.Start(new ProcessStartInfo
{
- FileName = Challenge.HttpResourceUrl,
+ FileName = challenge.HttpResourceUrl,
UseShellExecute = true
});
await _input.Wait();
@@ -107,8 +109,8 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
string? foundValue = null;
try
{
- var value = await WarmupSite();
- if (Equals(value, Challenge.HttpResourceValue))
+ var value = await WarmupSite(challenge);
+ if (Equals(value, challenge.HttpResourceValue))
{
_log.Information("Preliminary validation looks good, but the ACME server will be more thorough");
}
@@ -116,7 +118,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
{
_log.Warning("Preliminary validation failed, the server answered '{value}' instead of '{expected}'. The ACME server might have a different perspective",
foundValue ?? "(null)",
- Challenge.HttpResourceValue);
+ challenge.HttpResourceValue);
}
}
catch (HttpRequestException hrex)
@@ -130,15 +132,22 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
}
/// <summary>
+ /// Default commit function, doesn't do anything because
+ /// default doesn't do parallel operation
+ /// </summary>
+ /// <returns></returns>
+ public override Task Commit() => Task.CompletedTask;
+
+ /// <summary>
/// Warm up the target site, giving the application a little
/// time to start up before the validation request comes in.
/// Mostly relevant to classic FileSystem validation
/// </summary>
/// <param name="uri"></param>
- private async Task<string> WarmupSite()
+ private async Task<string> WarmupSite(Http01ChallengeValidationDetails challenge)
{
using var client = _proxy.GetHttpClient(false);
- var response = await client.GetAsync(Challenge.HttpResourceUrl);
+ var response = await client.GetAsync(challenge.HttpResourceUrl);
return await response.Content.ReadAsStringAsync();
}
@@ -147,14 +156,18 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
/// </summary>
/// <param name="answerPath">where the answerFile should be located</param>
/// <param name="fileContents">the contents of the file to write</param>
- private void WriteAuthorizationFile()
+ private void WriteAuthorizationFile(Http01ChallengeValidationDetails challenge)
{
if (_path == null)
{
throw new InvalidOperationException();
}
- WriteFile(CombinePath(_path, Challenge.HttpResourcePath), Challenge.HttpResourceValue);
- _challengeWritten = true;
+ var path = CombinePath(_path, challenge.HttpResourcePath);
+ WriteFile(path, challenge.HttpResourceValue);
+ if (!_filesWritten.Contains(path))
+ {
+ _filesWritten.Add(path);
+ }
}
/// <summary>
@@ -163,7 +176,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
/// <param name="target"></param>
/// <param name="answerPath"></param>
/// <param name="token"></param>
- private void WriteWebConfig()
+ private void WriteWebConfig(Http01ChallengeValidationDetails challenge)
{
if (_path == null)
{
@@ -173,13 +186,20 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
{
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;
- }
+ var partialPath = challenge.HttpResourcePath.Split('/').Last();
+ var destination = CombinePath(_path, challenge.HttpResourcePath.Replace(partialPath, "web.config"));
+ if (!_filesWritten.Contains(destination))
+ {
+ var content = GetWebConfig().Value;
+ if (content != null)
+ {
+ _log.Debug("Writing web.config");
+ WriteFile(destination, content);
+ _filesWritten.Add(destination);
+ }
+
+ }
+ }
catch (Exception ex)
{
_log.Warning("Unable to write web.config: {ex}", ex.Message); ;
@@ -191,66 +211,16 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
/// Get the template for the web.config
/// </summary>
/// <returns></returns>
- private string GetWebConfig() => File.ReadAllText(TemplateWebConfig);
-
- /// <summary>
- /// Can be used to write out server specific configuration, to handle extensionless files etc.
- /// </summary>
- /// <param name="target"></param>
- /// <param name="answerPath"></param>
- /// <param name="token"></param>
- private void DeleteWebConfig()
- {
- if (_path == null)
- {
- throw new InvalidOperationException();
- }
- if (_webConfigWritten)
- {
- _log.Debug("Deleting web.config");
- var partialPath = Challenge.HttpResourcePath.Split('/').Last();
- var destination = CombinePath(_path, Challenge.HttpResourcePath.Replace(partialPath, "web.config"));
- DeleteFile(destination);
- }
- }
-
- /// <summary>
- /// Should delete any authorizations
- /// </summary>
- /// <param name="answerPath">where the answerFile should be located</param>
- /// <param name="token">the token</param>
- /// <param name="webRootPath">the website root path</param>
- /// <param name="filePath">the file path for the authorization file</param>
- private void DeleteAuthorization()
- {
+ private Lazy<string?> GetWebConfig() => new Lazy<string?>(() => {
try
{
- if (_path != null && _challengeWritten)
- {
- _log.Debug("Deleting answer");
- var path = CombinePath(_path, Challenge.HttpResourcePath);
- var partialPath = Challenge.HttpResourcePath.Split('/').Last();
- DeleteFile(path);
- if (_settings.Validation.CleanupFolders)
- {
- path = path.Replace($"{PathSeparator}{partialPath}", "");
- if (DeleteFolderIfEmpty(path))
- {
- var idx = path.LastIndexOf(PathSeparator);
- if (idx >= 0)
- {
- path = path.Substring(0, path.LastIndexOf(PathSeparator));
- DeleteFolderIfEmpty(path);
- }
- }
- }
- }
- }
- catch (Exception ex)
+ return File.ReadAllText(TemplateWebConfig);
+ }
+ catch
{
- _log.Warning("Error occured while deleting folder structure. Error: {@ex}", ex);
+ return null;
}
- }
+ });
/// <summary>
/// Combine root path with relative path
@@ -271,11 +241,11 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
- private bool DeleteFolderIfEmpty(string path)
+ private async Task<bool> DeleteFolderIfEmpty(string path)
{
- if (IsEmpty(path))
+ if (await IsEmpty(path))
{
- DeleteFolder(path);
+ await DeleteFolder(path);
return true;
}
else
@@ -291,44 +261,78 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
/// <param name="root"></param>
/// <param name="path"></param>
/// <param name="content"></param>
- protected abstract void WriteFile(string path, string content);
+ protected abstract Task WriteFile(string path, string content);
/// <summary>
/// Delete file from specific location
/// </summary>
/// <param name="root"></param>
/// <param name="path"></param>
- protected abstract void DeleteFile(string path);
+ protected abstract Task DeleteFile(string path);
/// <summary>
/// Check if folder is empty
/// </summary>
/// <param name="root"></param>
/// <param name="path"></param>
- protected abstract bool IsEmpty(string path);
+ protected abstract Task<bool> IsEmpty(string path);
/// <summary>
/// Delete folder if not empty
/// </summary>
/// <param name="root"></param>
/// <param name="path"></param>
- protected abstract void DeleteFolder(string path);
+ protected abstract Task DeleteFolder(string path);
/// <summary>
/// Refresh
/// </summary>
/// <param name="scheduled"></param>
/// <returns></returns>
- protected virtual void Refresh() { }
+ protected virtual void Refresh(TargetPart targetPart) { }
/// <summary>
/// Dispose
/// </summary>
- public override Task CleanUp()
+ public override async Task CleanUp()
{
- DeleteWebConfig();
- DeleteAuthorization();
- return Task.CompletedTask;
+ try
+ {
+ if (_path != null)
+ {
+ var folders = new List<string>();
+ foreach (var file in _filesWritten)
+ {
+ _log.Debug("Deleting files");
+ await DeleteFile(file);
+ var folder = file.Substring(0, file.LastIndexOf(PathSeparator));
+ if (!folders.Contains(folder))
+ {
+ folders.Add(folder);
+ }
+ }
+ if (_settings.Validation.CleanupFolders)
+ {
+ _log.Debug("Deleting empty folders");
+ foreach (var folder in folders)
+ {
+ if (await DeleteFolderIfEmpty(folder))
+ {
+ var idx = folder.LastIndexOf(PathSeparator);
+ if (idx >= 0)
+ {
+ var parent = folder.Substring(0, folder.LastIndexOf(PathSeparator));
+ await DeleteFolderIfEmpty(parent);
+ }
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Warning("Error occured while deleting folder structure. Error: {@ex}", ex);
+ }
}
}
}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationParameters.cs b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationParameters.cs
index b1958b4..7092cdd 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationParameters.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationParameters.cs
@@ -7,9 +7,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
{
public ISettingsService Settings { get; private set; }
public Renewal Renewal { get; private set; }
- public TargetPart TargetPart { get; private set; }
public RunLevel RunLevel { get; private set; }
- public string Identifier { get; private set; }
public ILogService LogService { get; private set; }
public IInputService InputService { get; private set; }
public ProxyService ProxyService { get; private set; }
@@ -20,14 +18,10 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
ISettingsService settings,
ProxyService proxy,
Renewal renewal,
- TargetPart target,
- RunLevel runLevel,
- string identifier)
+ RunLevel runLevel)
{
Renewal = renewal;
- TargetPart = target;
RunLevel = runLevel;
- Identifier = identifier;
Settings = settings;
ProxyService = proxy;
LogService = log;
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHosting.cs b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHosting.cs
index 6e165af..c81966f 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHosting.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHosting.cs
@@ -1,7 +1,9 @@
using ACMESharp.Authorizations;
+using PKISharp.WACS.Context;
+using PKISharp.WACS.Plugins.Interfaces;
using PKISharp.WACS.Services;
using System;
-using System.Collections.Generic;
+using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Threading.Tasks;
@@ -13,12 +15,18 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
internal const int DefaultHttpValidationPort = 80;
internal const int DefaultHttpsValidationPort = 443;
+ private readonly object _listenerLock = new object();
private HttpListener? _listener;
- private readonly Dictionary<string, string> _files;
+ private readonly ConcurrentDictionary<string, string> _files;
private readonly SelfHostingOptions _options;
private readonly ILogService _log;
private readonly IUserRoleService _userRoleService;
+ /// <summary>
+ /// We can answer requests for multiple domains
+ /// </summary>
+ public override ParallelOperations Parallelism => ParallelOperations.Answer | ParallelOperations.Prepare;
+
private bool HasListener => _listener != null;
private HttpListener Listener
{
@@ -37,11 +45,11 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
{
_log = log;
_options = options;
- _files = new Dictionary<string, string>();
+ _files = new ConcurrentDictionary<string, string>();
_userRoleService = userRoleService;
}
- public async Task ReceiveRequests()
+ private async Task ReceiveRequests()
{
while (Listener.IsListening)
{
@@ -61,42 +69,61 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
}
}
- public override Task CleanUp()
+ public override Task PrepareChallenge(ValidationContext context, Http01ChallengeValidationDetails challenge)
+ {
+ // Add validation file
+ _files.GetOrAdd("/" + challenge.HttpResourcePath, challenge.HttpResourceValue);
+ return Task.CompletedTask;
+ }
+
+ public override Task Commit()
{
- if (HasListener)
+ // Create listener if it doesn't exist yet
+ lock (_listenerLock)
{
- try
- {
- Listener.Stop();
- Listener.Close();
- }
- catch
+ if (_listener == null)
{
+ var protocol = _options.Https == true ? "https" : "http";
+ var port = _options.Port ?? (_options.Https == true ?
+ DefaultHttpsValidationPort :
+ DefaultHttpValidationPort);
+ var prefix = $"{protocol}://+:{port}/.well-known/acme-challenge/";
+ try
+ {
+ Listener = new HttpListener();
+ Listener.Prefixes.Add(prefix);
+ Listener.Start();
+ Task.Run(ReceiveRequests);
+ }
+ catch
+ {
+ _log.Error("Unable to activate listener, this may be because of insufficient rights or a non-Microsoft webserver using port {port}", port);
+ throw;
+ }
}
}
return Task.CompletedTask;
}
- public override Task PrepareChallenge()
+ public override Task CleanUp()
{
- _files.Add("/" + Challenge.HttpResourcePath, Challenge.HttpResourceValue);
- var protocol = _options.Https == true ? "https" : "http";
- var port = _options.Port ?? (_options.Https == true ?
- DefaultHttpsValidationPort :
- DefaultHttpValidationPort);
- var prefix = $"{protocol}://+:{port}/.well-known/acme-challenge/";
- try
- {
- Listener = new HttpListener();
- Listener.Prefixes.Add(prefix);
- Listener.Start();
- Task.Run(ReceiveRequests);
- }
- catch
+ // Cleanup listener if nobody else has done it yet
+ lock (_listenerLock)
{
- _log.Error("Unable to activate listener, this may be because of insufficient rights or a non-Microsoft webserver using port {port}", port);
- throw;
+ if (HasListener)
+ {
+ try
+ {
+ Listener.Stop();
+ Listener.Close();
+ }
+ finally
+ {
+ _listener = null;
+ }
+ }
}
+
return Task.CompletedTask;
}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs b/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs
index c3d5c08..a7a88ee 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs
@@ -1,5 +1,6 @@
using PKISharp.WACS.Clients;
using System.Linq;
+using System.Threading.Tasks;
namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
{
@@ -11,12 +12,12 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
protected override char PathSeparator => '/';
- protected override void DeleteFile(string path) => _sshFtpClient.Delete(path, SshFtpClient.FileType.File);
+ protected override async Task DeleteFile(string path) => _sshFtpClient.Delete(path, SshFtpClient.FileType.File);
- protected override void DeleteFolder(string path) => _sshFtpClient.Delete(path, SshFtpClient.FileType.Directory);
+ protected override async Task DeleteFolder(string path) => _sshFtpClient.Delete(path, SshFtpClient.FileType.Directory);
- protected override bool IsEmpty(string path) => !_sshFtpClient.GetFiles(path).Any();
+ protected override async Task<bool> IsEmpty(string path) => !_sshFtpClient.GetFiles(path).Any();
- protected override void WriteFile(string path, string content) => _sshFtpClient.Upload(path, content);
+ protected override async Task WriteFile(string path, string content) => _sshFtpClient.Upload(path, content);
}
}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDav.cs b/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDav.cs
index 990f338..f1adaea 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDav.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDav.cs
@@ -1,4 +1,6 @@
-using PKISharp.WACS.Client;
+using ACMESharp.Authorizations;
+using PKISharp.WACS.Client;
+using PKISharp.WACS.Context;
using PKISharp.WACS.Services;
using System.Threading.Tasks;
@@ -13,15 +15,15 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
RunLevel runLevel, ProxyService proxy) :
base(options, runLevel, pars) => _webdavClient = new WebDavClientWrapper(_options.Credential, pars.LogService, proxy);
- protected override void DeleteFile(string path) => _webdavClient.Delete(path);
+ protected override async Task DeleteFile(string path) => _webdavClient.Delete(path);
- protected override void DeleteFolder(string path) => _webdavClient.Delete(path);
+ protected override async Task DeleteFolder(string path) => _webdavClient.Delete(path);
- protected override bool IsEmpty(string path) => !_webdavClient.IsEmpty(path);
+ protected override async Task<bool> IsEmpty(string path) => !_webdavClient.IsEmpty(path);
protected override char PathSeparator => '/';
- protected override void WriteFile(string path, string content) => _webdavClient.Upload(path, content);
+ protected override async Task WriteFile(string path, string content) => _webdavClient.Upload(path, content);
public override Task CleanUp()
{
base.CleanUp();
diff --git a/src/main.lib/Plugins/ValidationPlugins/Tls/SelfHosting/SelfHosting.cs b/src/main.lib/Plugins/ValidationPlugins/Tls/SelfHosting/SelfHosting.cs
index d43d58b..f7cea99 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Tls/SelfHosting/SelfHosting.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Tls/SelfHosting/SelfHosting.cs
@@ -1,5 +1,8 @@
using ACMESharp.Authorizations;
using Org.BouncyCastle.Asn1;
+using PKISharp.WACS.Context;
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Plugins.Interfaces;
using PKISharp.WACS.Services;
using System;
using System.Collections.Generic;
@@ -20,7 +23,6 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Tls
internal const int DefaultValidationPort = 443;
private TcpListener? _listener;
private X509Certificate2? _certificate;
- private readonly string _identifier;
private readonly SelfHostingOptions _options;
private readonly ILogService _log;
private readonly IUserRoleService _userRoleService;
@@ -39,9 +41,8 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Tls
set => _listener = value;
}
- public SelfHosting(ILogService log, string identifier, SelfHostingOptions options, IUserRoleService userRoleService)
+ public SelfHosting(ILogService log, SelfHostingOptions options, IUserRoleService userRoleService)
{
- _identifier = identifier;
_log = log;
_options = options;
_userRoleService = userRoleService;
@@ -69,6 +70,8 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Tls
}
}
+ public override Task Commit() => Task.CompletedTask;
+
public override Task CleanUp()
{
try
@@ -82,12 +85,12 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Tls
return Task.CompletedTask;
}
- public override Task PrepareChallenge()
+ public override Task PrepareChallenge(ValidationContext context, TlsAlpn01ChallengeValidationDetails challenge)
{
try
{
using var rsa = RSA.Create(2048);
- var name = new X500DistinguishedName($"CN={_identifier}");
+ var name = new X500DistinguishedName($"CN={context.Identifier}");
var request = new CertificateRequest(
name,
@@ -96,7 +99,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Tls
RSASignaturePadding.Pkcs1);
using var sha = SHA256.Create();
- var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(Challenge.TokenValue));
+ var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(challenge.TokenValue));
request.CertificateExtensions.Add(
new X509Extension(
new AsnEncodedData("1.3.6.1.5.5.7.1.31",
@@ -104,7 +107,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Tls
true));
var sanBuilder = new SubjectAlternativeNameBuilder();
- sanBuilder.AddDnsName(_identifier);
+ sanBuilder.AddDnsName(context.Identifier);
request.CertificateExtensions.Add(sanBuilder.Build());
_certificate = request.CreateSelfSigned(
@@ -112,8 +115,8 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Tls
new DateTimeOffset(DateTime.UtcNow.AddDays(1)));
_certificate = new X509Certificate2(
- _certificate.Export(X509ContentType.Pfx, _identifier),
- _identifier,
+ _certificate.Export(X509ContentType.Pfx, context.Identifier),
+ context.Identifier,
X509KeyStorageFlags.MachineKeySet);
_listener = new TcpListener(IPAddress.Any, _options.Port ?? DefaultValidationPort);
diff --git a/src/main.lib/Plugins/ValidationPlugins/Validation.cs b/src/main.lib/Plugins/ValidationPlugins/Validation.cs
index cb9da68..d846e62 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Validation.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Validation.cs
@@ -1,4 +1,5 @@
using ACMESharp.Authorizations;
+using PKISharp.WACS.Context;
using PKISharp.WACS.Plugins.Interfaces;
using System;
using System.Threading.Tasks;
@@ -10,31 +11,15 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
/// </summary>
public abstract class Validation<TChallenge> : IValidationPlugin where TChallenge : class, IChallengeValidationDetails
{
- public bool HasChallenge => _challenge != null;
- public TChallenge Challenge
- {
- get
- {
- if (_challenge == null)
- {
- throw new InvalidOperationException();
- }
- return _challenge;
- }
- }
- private TChallenge? _challenge;
-
-
/// <summary>
/// Handle the challenge
/// </summary>
/// <param name="challenge"></param>
- public async Task PrepareChallenge(IChallengeValidationDetails challenge)
+ public async Task PrepareChallenge(ValidationContext context)
{
- if (challenge is TChallenge typed)
+ if (context.ChallengeDetails is TChallenge typed)
{
- _challenge = typed;
- await PrepareChallenge();
+ await PrepareChallenge(context, typed);
}
else
{
@@ -46,16 +31,24 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
/// Handle the challenge
/// </summary>
/// <param name="challenge"></param>
- public abstract Task PrepareChallenge();
+ public abstract Task PrepareChallenge(ValidationContext context, TChallenge typed);
/// <summary>
- /// Clean up after validation
+ /// Commit changes
/// </summary>
+ /// <returns></returns>
+ public abstract Task Commit();
+
public abstract Task CleanUp();
/// <summary>
/// Is the plugin currently disabled
/// </summary>
public virtual (bool, string?) Disabled => (false, null);
+
+ /// <summary>
+ /// No parallelism by default
+ /// </summary>
+ public virtual ParallelOperations Parallelism => ParallelOperations.None;
}
}
diff --git a/src/main.lib/RenewalCreator.cs b/src/main.lib/RenewalCreator.cs
index 404ee76..0210ca2 100644
--- a/src/main.lib/RenewalCreator.cs
+++ b/src/main.lib/RenewalCreator.cs
@@ -363,14 +363,14 @@ namespace PKISharp.WACS
{
goto retry;
}
- _exceptionHandler.HandleException(message: $"Create certificate failed: {string.Join(", ", result.ErrorMessages)}");
+ _exceptionHandler.HandleException(message: $"Create certificate failed: {string.Join("\n\t- ", result.ErrorMessages)}");
}
else
{
try
{
_renewalStore.Save(renewal, result);
- _notification.NotifyCreated(renewal, _log.Lines);
+ await _notification.NotifyCreated(renewal, _log.Lines);
}
catch (Exception ex)
{
diff --git a/src/main.lib/RenewalExecutor.cs b/src/main.lib/RenewalExecutor.cs
index aa38dca..5f17bef 100644
--- a/src/main.lib/RenewalExecutor.cs
+++ b/src/main.lib/RenewalExecutor.cs
@@ -1,6 +1,8 @@
using Autofac;
+using Newtonsoft.Json.Schema;
using PKISharp.WACS.Clients.Acme;
using PKISharp.WACS.Configuration;
+using PKISharp.WACS.Context;
using PKISharp.WACS.DomainObjects;
using PKISharp.WACS.Extensions;
using PKISharp.WACS.Plugins.Base.Options;
@@ -10,7 +12,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using acme = ACMESharp.Protocol.Resources;
namespace PKISharp.WACS
{
@@ -25,33 +26,15 @@ namespace PKISharp.WACS
private readonly ILogService _log;
private readonly IInputService _input;
private readonly ExceptionHandler _exceptionHandler;
-
- /// <summary>
- /// Common objects used throughout the renewal process
- /// </summary>
- private class ExecutionContext
- {
- public ILifetimeScope Scope { get; private set; }
- public Order Order { get; private set; }
- public RunLevel RunLevel { get; private set; }
- public RenewResult Result { get; private set; }
- public Target Target => Order.Target;
- public Renewal Renewal => Order.Renewal;
-
- public ExecutionContext(ILifetimeScope scope, Order order, RunLevel runLevel, RenewResult result)
- {
- Scope = scope;
- Order = order;
- RunLevel = runLevel;
- Result = result;
- }
- }
+ private readonly RenewalValidator _validator;
public RenewalExecutor(
MainArguments args, IAutofacBuilder scopeBuilder,
ILogService log, IInputService input,
+ RenewalValidator validator,
ExceptionHandler exceptionHandler, IContainer container)
{
+ _validator = validator;
_args = args;
_scopeBuilder = scopeBuilder;
_log = log;
@@ -189,7 +172,7 @@ namespace PKISharp.WACS
var context = new ExecutionContext(execute, order, runLevel, result);
// Authorize the order (validation)
- await AuthorizeOrder(context);
+ await _validator.AuthorizeOrder(context, runLevel);
if (context.Result.Success)
{
// Execute final steps (CSR, store, install)
@@ -200,54 +183,6 @@ namespace PKISharp.WACS
}
/// <summary>
- /// Answer all the challenges in the order
- /// </summary>
- /// <param name="execute"></param>
- /// <param name="order"></param>
- /// <param name="result"></param>
- /// <param name="runLevel"></param>
- /// <returns></returns>
- private async Task AuthorizeOrder(ExecutionContext context)
- {
- // Sanity check
- if (context.Order.Details == null)
- {
- context.Result.AddErrorMessage($"Unable to create order");
- return;
- }
-
- // Answer the challenges
- var client = context.Scope.Resolve<AcmeClient>();
- var authorizations = context.Order.Details.Payload.Authorizations.ToList();
- foreach (var authorizationUri in authorizations)
- {
- _log.Verbose("Handle authorization {n}/{m}",
- authorizations.IndexOf(authorizationUri) + 1,
- authorizations.Count);
-
- // Get authorization challenge details from server
- var authorization = await client.GetAuthorizationDetails(authorizationUri);
-
- // Find a targetPart that matches the challenge
- var targetPart = context.Target.Parts.
- FirstOrDefault(tp => tp.GetHosts(false).
- Any(h => authorization.Identifier.Value == h.Replace("*.", "")));
- if (targetPart == null)
- {
- context.Result.AddErrorMessage("Unable to match challenge to target");
- return;
- }
-
- // Run the validation plugin
- await HandleChallenge(context, targetPart, authorization);
- if (!context.Result.Success)
- {
- break;
- }
- }
- }
-
- /// <summary>
/// Steps to take on succesful (re)authorization
/// </summary>
/// <param name="partialTarget"></param>
@@ -395,156 +330,6 @@ namespace PKISharp.WACS
context.Result.AddErrorMessage(message);
}
}
-
- /// <summary>
- /// Make sure we have authorization for every host in target
- /// </summary>
- /// <param name="target"></param>
- /// <returns></returns>
- private async Task HandleChallenge(ExecutionContext context, TargetPart targetPart, acme.Authorization authorization)
- {
- var valid = false;
- var client = context.Scope.Resolve<AcmeClient>();
- var identifier = authorization.Identifier.Value;
- var options = context.Renewal.ValidationPluginOptions;
- IValidationPlugin? validationPlugin = null;
- using var validation = _scopeBuilder.Validation(context.Scope, options, targetPart, identifier);
- try
- {
- if (authorization.Status == AcmeClient.AuthorizationValid)
- {
- _log.Information("Cached authorization result for {identifier}: {Status}", identifier, authorization.Status);
- if (!context.RunLevel.HasFlag(RunLevel.Test) &&
- !context.RunLevel.HasFlag(RunLevel.IgnoreCache))
- {
- return;
- }
- // Used to make --force or --test re-validation errors non-fatal
- _log.Information("Handling challenge anyway because --test and/or --force is active");
- valid = true;
- }
-
- _log.Information("Authorize identifier {identifier}", identifier);
- _log.Verbose("Initial authorization status: {status}", authorization.Status);
- _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 (valid)
- {
- var usedType = authorization.Challenges.
- Where(x => x.Status == AcmeClient.ChallengeValid).
- FirstOrDefault();
- _log.Warning("Expected challenge type {type} not available for {identifier}, already validated using {valided}.",
- options.ChallengeType,
- authorization.Identifier.Value,
- usedType?.Type ?? "[unknown]");
- return;
- }
- else
- {
- _log.Error("Expected challenge type {type} not available for {identifier}.",
- options.ChallengeType,
- authorization.Identifier.Value);
- context.Result.AddErrorMessage("Expected challenge type not available", !valid);
- return;
- }
- }
- else
- {
- _log.Verbose("Initial challenge status: {status}", challenge.Status);
- if (challenge.Status == AcmeClient.ChallengeValid)
- {
- // We actually should not get here because if one of the
- // challenges is valid, the authorization itself should also
- // be valid.
- if (!context.RunLevel.HasFlag(RunLevel.Test) &&
- !context.RunLevel.HasFlag(RunLevel.IgnoreCache))
- {
- _log.Information("Cached challenge result: {Status}", authorization.Status);
- return;
- }
- }
- }
-
- // We actually have to do validation now
- 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");
- context.Result.AddErrorMessage("Validation plugin not found or not created", !valid);
- return;
- }
- var (disabled, disabledReason) = validationPlugin.Disabled;
- if (disabled)
- {
- _log.Error($"Validation plugin is not available. {disabledReason}");
- context.Result.AddErrorMessage("Validation plugin is not available", !valid);
- return;
- }
- _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");
- context.Result.AddErrorMessage("Error preparing for challenge answer", !valid);
- return;
- }
-
- _log.Debug("Submitting challenge answer");
- challenge = await client.AnswerChallenge(challenge);
- if (challenge.Status != AcmeClient.ChallengeValid)
- {
- if (challenge.Error != null)
- {
- _log.Error(challenge.Error.ToString());
- }
- _log.Error("Authorization result: {Status}", challenge.Status);
- context.Result.AddErrorMessage(challenge.Error?.ToString() ?? "Unspecified error", !valid);
- return;
- }
- else
- {
- _log.Information("Authorization result: {Status}", challenge.Status);
- return;
- }
- }
- catch (Exception ex)
- {
- _log.Error("Error authorizing {renewal}", targetPart);
- var message = _exceptionHandler.HandleException(ex);
- context.Result.AddErrorMessage(message, !valid);
- }
- finally
- {
- if (validationPlugin != null)
- {
- try
- {
- _log.Verbose("Starting post-validation cleanup");
- await validationPlugin.CleanUp();
- _log.Verbose("Post-validation cleanup was succesful");
- }
- catch (Exception ex)
- {
- _log.Warning("An error occured during post-validation cleanup: {ex}", ex.Message);
- }
- }
- }
- }
+
}
}
diff --git a/src/main.lib/RenewalManager.cs b/src/main.lib/RenewalManager.cs
index 93ef1df..b4a4aff 100644
--- a/src/main.lib/RenewalManager.cs
+++ b/src/main.lib/RenewalManager.cs
@@ -400,7 +400,7 @@ namespace PKISharp.WACS
_input.Show(null, "Please input friendly name to filter renewals by. " + IISArgumentsProvider.PatternExamples);
var rawInput = await _input.RequestString("Friendly name");
var ret = new List<Renewal>();
- var regex = new Regex(rawInput.PatternToRegex());
+ var regex = new Regex(rawInput.PatternToRegex(), RegexOptions.IgnoreCase);
foreach (var r in current)
{
if (regex.Match(r.LastFriendlyName).Success)
@@ -494,18 +494,18 @@ namespace PKISharp.WACS
_renewalStore.Save(renewal, result);
if (result.Success)
{
- notification.NotifySuccess(renewal, _log.Lines);
+ await notification.NotifySuccess(renewal, _log.Lines);
}
else
{
- notification.NotifyFailure(runLevel, renewal, result.ErrorMessages, _log.Lines);
+ await notification.NotifyFailure(runLevel, renewal, result.ErrorMessages, _log.Lines);
}
}
}
catch (Exception ex)
{
_exceptionHandler.HandleException(ex);
- notification.NotifyFailure(runLevel, renewal, new List<string> { ex.Message }, _log.Lines);
+ await notification.NotifyFailure(runLevel, renewal, new List<string> { ex.Message }, _log.Lines);
}
}
@@ -633,4 +633,4 @@ namespace PKISharp.WACS
#endregion
}
-} \ No newline at end of file
+}
diff --git a/src/main.lib/RenewalValidator.cs b/src/main.lib/RenewalValidator.cs
new file mode 100644
index 0000000..3421d07
--- /dev/null
+++ b/src/main.lib/RenewalValidator.cs
@@ -0,0 +1,447 @@
+using ACMESharp.Protocol.Resources;
+using Autofac;
+using PKISharp.WACS.Clients.Acme;
+using PKISharp.WACS.Context;
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Extensions;
+using PKISharp.WACS.Plugins.Base.Options;
+using PKISharp.WACS.Plugins.Interfaces;
+using PKISharp.WACS.Services;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Policy;
+using System.Threading.Tasks;
+
+namespace PKISharp.WACS
+{
+ /// <summary>
+ /// This part of the code handles the actual creation/renewal of ACME certificates
+ /// </summary>
+ internal class RenewalValidator
+ {
+ private readonly IAutofacBuilder _scopeBuilder;
+ private readonly ILogService _log;
+ private readonly ISettingsService _settings;
+ private readonly ExceptionHandler _exceptionHandler;
+ public RenewalValidator(IAutofacBuilder scopeBuilder, ISettingsService settings, ILogService log, ExceptionHandler exceptionHandler)
+ {
+ _scopeBuilder = scopeBuilder;
+ _log = log;
+ _exceptionHandler = exceptionHandler;
+ _settings = settings;
+ }
+
+ /// <summary>
+ /// Answer all the challenges in the order
+ /// </summary>
+ /// <param name="execute"></param>
+ /// <param name="order"></param>
+ /// <param name="result"></param>
+ /// <param name="runLevel"></param>
+ /// <returns></returns>
+ public async Task AuthorizeOrder(ExecutionContext context, RunLevel runLevel)
+ {
+ // Sanity check
+ if (context.Order.Details == null)
+ {
+ context.Result.AddErrorMessage($"Unable to create order");
+ return;
+ }
+
+ if (context.Order.Details.Payload.Status == AcmeClient.OrderInvalid)
+ {
+ context.Result.AddErrorMessage($"Created order was invalid");
+ return;
+ }
+
+ // Maybe validation is not needed at all
+ var orderValid = false;
+ if (context.Order.Details.Payload.Status == AcmeClient.OrderReady ||
+ context.Order.Details.Payload.Status == AcmeClient.OrderValid)
+ {
+ if (!runLevel.HasFlag(RunLevel.Test) &&
+ !runLevel.HasFlag(RunLevel.IgnoreCache))
+ {
+ return;
+ }
+ else
+ {
+ orderValid = true;
+ }
+ }
+
+ // Get validation plugin
+ var options = context.Renewal.ValidationPluginOptions;
+ var validationScope = _scopeBuilder.Validation(context.Scope, options);
+ var validationPlugin = validationScope.Resolve<IValidationPlugin>();
+ if (validationPlugin == null)
+ {
+ _log.Error("Validation plugin not found or not created");
+ context.Result.AddErrorMessage("Validation plugin not found or not created", !orderValid);
+ return;
+ }
+ var (disabled, disabledReason) = validationPlugin.Disabled;
+ if (disabled)
+ {
+ _log.Error($"Validation plugin is not available. {disabledReason}");
+ context.Result.AddErrorMessage("Validation plugin is not available", !orderValid);
+ return;
+ }
+
+ // Get authorization details
+ var authorizations = context.Order.Details.Payload.Authorizations.ToList();
+ var contextParamTasks = authorizations.Select(authorizationUri => GetValidationContextParameters(context, authorizationUri, options, orderValid));
+ var contextParams = (await Task.WhenAll(contextParamTasks)).OfType<ValidationContextParameters>().ToList();
+ if (!context.Result.Success)
+ {
+ return;
+ }
+
+ if (_settings.Validation.DisableMultiThreading == true ||
+ validationPlugin.Parallelism == ParallelOperations.None)
+ {
+ await SerialValidation(context, contextParams);
+ }
+ else
+ {
+ await ParallelValidation(validationPlugin.Parallelism, validationScope, context, contextParams);
+ }
+ }
+
+ /// <summary>
+ /// Handle multiple validations in parallel
+ /// </summary>
+ /// <returns></returns>
+ private async Task ParallelValidation(ParallelOperations level, ILifetimeScope scope, ExecutionContext context, List<ValidationContextParameters> parameters)
+ {
+ var contexts = parameters.Select(parameter => new ValidationContext(scope, parameter)).ToList();
+ var plugin = contexts.First().ValidationPlugin;
+
+ // Prepare for challenge answer
+ if (level.HasFlag(ParallelOperations.Prepare))
+ {
+ // Parallel
+ _log.Verbose("Handle {n} preparation(s)", contexts.Count);
+ var prepareTasks = contexts.Select(vc => PrepareChallengeAnswer(vc, context.RunLevel));
+ await Task.WhenAll(prepareTasks);
+ foreach (var ctx in contexts)
+ {
+ TransferErrors(ctx, context.Result);
+ }
+ if (!context.Result.Success)
+ {
+ return;
+ }
+ }
+ else
+ {
+ // Serial
+ foreach (var ctx in contexts)
+ {
+ await PrepareChallengeAnswer(ctx, context.RunLevel);
+
+ TransferErrors(ctx, context.Result);
+ if (!context.Result.Success)
+ {
+ return;
+ }
+ }
+ }
+
+ // Commit
+ var commited = await CommitValidation(plugin);
+ if (!commited)
+ {
+ context.Result.AddErrorMessage("Commit failed");
+ return;
+ }
+
+ // Submit challenge answer
+ var contextsWithChallenges = contexts.Where(x => x.ChallengeDetails != null);
+ if (contextsWithChallenges.Any())
+ {
+ if (level.HasFlag(ParallelOperations.Answer))
+ {
+ // Parallel
+ _log.Verbose("Handle {n} answers(s)", contextsWithChallenges.Count());
+ var answerTasks = contexts.Select(vc => AnswerChallenge(vc));
+ await Task.WhenAll(answerTasks);
+ foreach (var ctx in contextsWithChallenges)
+ {
+ TransferErrors(ctx, context.Result);
+ }
+ if (!context.Result.Success)
+ {
+ return;
+ }
+ }
+ else
+ {
+ // Serial
+ foreach (var ctx in contextsWithChallenges)
+ {
+ await AnswerChallenge(ctx);
+ TransferErrors(ctx, context.Result);
+ if (!context.Result.Success)
+ {
+ return;
+ }
+ }
+ }
+
+ // Cleanup
+ await CleanValidation(contexts.First().ValidationPlugin);
+ }
+ }
+
+ /// <summary>
+ /// Handle validation in serial order
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="parameters"></param>
+ /// <returns></returns>
+ private async Task SerialValidation(ExecutionContext context, List<ValidationContextParameters> parameters)
+ {
+ foreach (var parameter in parameters)
+ {
+ _log.Verbose("Handle authorization {n}/{m}",
+ parameters.IndexOf(parameter) + 1,
+ parameters.Count);
+ using var identifierScope = _scopeBuilder.Validation(context.Scope, context.Renewal.ValidationPluginOptions);
+ await ParallelValidation(ParallelOperations.None, identifierScope, context, new List<ValidationContextParameters> { parameter });
+ if (!context.Result.Success)
+ {
+ break;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Get information needed to construct a validation context (shared between serial and parallel mode)
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="authorizationUri"></param>
+ /// <param name="options"></param>
+ /// <returns></returns>
+ private async Task<ValidationContextParameters?> GetValidationContextParameters(ExecutionContext context, string authorizationUri, ValidationPluginOptions options, bool orderValid)
+ {
+ // Get authorization challenge details from server
+ var client = context.Scope.Resolve<AcmeClient>();
+ var authorization = default(Authorization);
+ try
+ {
+ authorization = await client.GetAuthorizationDetails(authorizationUri);
+ }
+ catch
+ {
+ context.Result.AddErrorMessage($"Unable to get authorization details from {authorizationUri}", !orderValid);
+ return null;
+ }
+
+ // Find a targetPart that matches the challenge
+ var targetPart = context.Target.Parts.
+ FirstOrDefault(tp => tp.GetHosts(false).
+ Any(h => authorization.Identifier.Value == h.Replace("*.", "")));
+
+ if (targetPart == null)
+ {
+ context.Result.AddErrorMessage($"Unable to match challenge {authorization.Identifier.Value} to target", !orderValid);
+ return null;
+ }
+
+ return new ValidationContextParameters(authorization, targetPart, options.ChallengeType, options.Name, orderValid);
+ }
+
+ /// <summary>
+ /// Move errors from a validation context up to the renewal result
+ /// </summary>
+ /// <param name="from"></param>
+ /// <param name="to"></param>
+ /// <param name="prefix"></param>
+ private void TransferErrors(ValidationContext from, RenewResult to)
+ {
+ from.ErrorMessages.ForEach(e => to.AddErrorMessage($"[{from.Identifier}] {e}", from.Success != true));
+ from.ErrorMessages.Clear();
+ }
+
+
+ /// <summary>
+ /// Make sure we have authorization for every host in target
+ /// </summary>
+ /// <param name="target"></param>
+ /// <returns></returns>
+ private async Task PrepareChallengeAnswer(ValidationContext context, RunLevel runLevel)
+ {
+ if (context.ValidationPlugin == null)
+ {
+ throw new InvalidOperationException();
+ }
+ var client = context.Scope.Resolve<AcmeClient>();
+ try
+ {
+ if (context.Authorization.Status == AcmeClient.AuthorizationValid)
+ {
+ _log.Information("[{identifier}] Cached authorization result: {Status}", context.Identifier, context.Authorization.Status);
+ if (!runLevel.HasFlag(RunLevel.Test) && !runLevel.HasFlag(RunLevel.IgnoreCache))
+ {
+ return;
+ }
+ // Used to make --force or --test re-validation errors non-fatal
+ _log.Information("[{identifier}] Handling challenge anyway because --test and/or --force is active", context.Identifier);
+ context.Success = true;
+ }
+
+ _log.Information("[{identifier}] Authorizing...", context.Identifier);
+ _log.Verbose("[{identifier}] Initial authorization status: {status}", context.Identifier, context.Authorization.Status);
+ _log.Verbose("[{identifier}] Challenge types available: {challenges}", context.Identifier, context.Authorization.Challenges.Select(x => x.Type ?? "[Unknown]"));
+ var challenge = context.Authorization.Challenges.FirstOrDefault(c => string.Equals(c.Type, context.ChallengeType, StringComparison.CurrentCultureIgnoreCase));
+ if (challenge == null)
+ {
+ if (context.Success == true)
+ {
+ var usedType = context.Authorization.Challenges.
+ Where(x => x.Status == AcmeClient.ChallengeValid).
+ FirstOrDefault();
+ _log.Warning("[{identifier}] Expected challenge type {type} not available, already validated using {valided}.",
+ context.Identifier,
+ context.ChallengeType,
+ usedType?.Type ?? "[unknown]");
+ return;
+ }
+ else
+ {
+ _log.Error("[{identifier}] Expected challenge type {type} not available.",
+ context.Identifier,
+ context.ChallengeType);
+ context.AddErrorMessage("Expected challenge type not available", context.Success == false);
+ return;
+ }
+ }
+ else
+ {
+ _log.Verbose("[{identifier}] Initial challenge status: {status}", context.Identifier, challenge.Status);
+ if (challenge.Status == AcmeClient.ChallengeValid)
+ {
+ // We actually should not get here because if one of the
+ // challenges is valid, the authorization itself should also
+ // be valid.
+ if (!runLevel.HasFlag(RunLevel.Test) && !runLevel.HasFlag(RunLevel.IgnoreCache))
+ {
+ _log.Information("[{identifier}] Cached challenge result: {Status}", context.Identifier, context.Authorization.Status);
+ return;
+ }
+ }
+ }
+ _log.Information("[{identifier}] Authorizing using {challengeType} validation ({name})",
+ context.Identifier,
+ context.ChallengeType,
+ context.PluginName);
+ try
+ {
+ // Now that we're going to call into PrepareChallenge, we will assume
+ // responsibility to also call CleanUp later, which is signalled by
+ // the Challenge propery being not null
+ context.ChallengeDetails = await client.DecodeChallengeValidation(context.Authorization, challenge);
+ context.Challenge = challenge;
+ await context.ValidationPlugin.PrepareChallenge(context);
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "[{identifier}] Error preparing for challenge answer", context.Identifier);
+ context.AddErrorMessage("Error preparing for challenge answer", context.Success == false);
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Error("[{identifier}] Error preparing challenge answer", context.Identifier);
+ var message = _exceptionHandler.HandleException(ex);
+ context.AddErrorMessage(message, context.Success == false);
+ }
+ }
+
+ /// <summary>
+ /// Make sure we have authorization for every host in target
+ /// </summary>
+ /// <param name="target"></param>
+ /// <returns></returns>
+ private async Task AnswerChallenge(ValidationContext validationContext)
+ {
+ if (validationContext.Challenge == null)
+ {
+ throw new InvalidOperationException();
+ }
+ try
+ {
+ _log.Debug("[{identifier}] Submitting challenge answer", validationContext.Identifier);
+ var client = validationContext.Scope.Resolve<AcmeClient>();
+ var updatedChallenge = await client.AnswerChallenge(validationContext.Challenge);
+ validationContext.Challenge = updatedChallenge;
+ if (updatedChallenge.Status != AcmeClient.ChallengeValid)
+ {
+ _log.Error("[{identifier}] Authorization result: {Status}", validationContext.Identifier, updatedChallenge.Status);
+ if (updatedChallenge.Error != null)
+ {
+ _log.Error("[{identifier}] {Error}", validationContext.Identifier, updatedChallenge.Error.ToString());
+
+ }
+ validationContext.AddErrorMessage("Validation failed", validationContext.Success == false);
+ return;
+ }
+ else
+ {
+ validationContext.Success = true;
+ _log.Information("[{identifier}] Authorization result: {Status}", validationContext.Identifier, updatedChallenge.Status);
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Error("[{identifier}] Error submitting challenge answer", validationContext.Identifier);
+ var message = _exceptionHandler.HandleException(ex);
+ validationContext.AddErrorMessage(message, validationContext.Success == false);
+ }
+ }
+
+ /// <summary>
+ /// Clean up after (succesful or unsuccesful) validation attempt
+ /// </summary>
+ /// <param name="validationContext"></param>
+ /// <returns></returns>
+ private async Task<bool> CommitValidation(IValidationPlugin validationPlugin)
+ {
+ try
+ {
+ _log.Verbose("Starting commit stage");
+ await validationPlugin.Commit();
+ _log.Verbose("Commit was succesful");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "An error occured while commiting validation configuration: {ex}", ex.Message);
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Clean up after (succesful or unsuccesful) validation attempt
+ /// </summary>
+ /// <param name="validationContext"></param>
+ /// <returns></returns>
+ private async Task CleanValidation(IValidationPlugin validationPlugin)
+ {
+ try
+ {
+ _log.Verbose("Starting post-validation cleanup");
+ await validationPlugin.CleanUp();
+ _log.Verbose("Post-validation cleanup was succesful");
+ }
+ catch (Exception ex)
+ {
+ _log.Warning("An error occured during post-validation cleanup: {ex}", ex.Message);
+ }
+ }
+ }
+}
diff --git a/src/main.lib/Services/AutofacBuilder.cs b/src/main.lib/Services/AutofacBuilder.cs
index 5a5e4f9..00080fd 100644
--- a/src/main.lib/Services/AutofacBuilder.cs
+++ b/src/main.lib/Services/AutofacBuilder.cs
@@ -202,19 +202,12 @@ namespace PKISharp.WACS.Services
/// <param name="target"></param>
/// <param name="identifier"></param>
/// <returns></returns>
- public ILifetimeScope Validation(ILifetimeScope execution, ValidationPluginOptions options, TargetPart target, string identifier)
+ public ILifetimeScope Validation(ILifetimeScope execution, ValidationPluginOptions options)
{
return execution.BeginLifetimeScope(builder =>
{
- builder.RegisterType<HttpValidationParameters>().
- WithParameters(new[] {
- new TypedParameter(typeof(string), identifier),
- new TypedParameter(typeof(TargetPart), target)
- });
+ builder.RegisterType<HttpValidationParameters>();
builder.RegisterType(options.Instance).
- WithParameters(new[] {
- new TypedParameter(typeof(string), identifier),
- }).
As<IValidationPlugin>().
SingleInstance();
});
diff --git a/src/main.lib/Services/Interfaces/IAutofacBuilder.cs b/src/main.lib/Services/Interfaces/IAutofacBuilder.cs
index c2a3893..eb11a39 100644
--- a/src/main.lib/Services/Interfaces/IAutofacBuilder.cs
+++ b/src/main.lib/Services/Interfaces/IAutofacBuilder.cs
@@ -50,6 +50,6 @@ namespace PKISharp.WACS.Services
/// <param name="target"></param>
/// <param name="identifier"></param>
/// <returns></returns>
- ILifetimeScope Validation(ILifetimeScope execution, ValidationPluginOptions options, TargetPart target, string identifier);
+ ILifetimeScope Validation(ILifetimeScope execution, ValidationPluginOptions options);
}
}
diff --git a/src/main.lib/Services/NotificationService.cs b/src/main.lib/Services/NotificationService.cs
index cff3803..7795fac 100644
--- a/src/main.lib/Services/NotificationService.cs
+++ b/src/main.lib/Services/NotificationService.cs
@@ -5,6 +5,7 @@ using Serilog.Events;
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
using System.Web;
namespace PKISharp.WACS.Services
@@ -33,7 +34,7 @@ namespace PKISharp.WACS.Services
/// </summary>
/// <param name="runLevel"></param>
/// <param name="renewal"></param>
- internal void NotifyCreated(Renewal renewal, IEnumerable<MemoryEntry> log)
+ internal async Task NotifyCreated(Renewal renewal, IEnumerable<MemoryEntry> log)
{
// Do not send emails when running interactively
_log.Information(
@@ -42,7 +43,7 @@ namespace PKISharp.WACS.Services
renewal.LastFriendlyName);
if (_settings.Notification.EmailOnSuccess)
{
- _email.Send(
+ await _email.Send(
$"Certificate {renewal.LastFriendlyName} created",
@$"<p>Certificate <b>{HttpUtility.HtmlEncode(renewal.LastFriendlyName)}</b> succesfully created.</p>
{NotificationInformation(renewal)}
@@ -56,7 +57,7 @@ namespace PKISharp.WACS.Services
/// </summary>
/// <param name="runLevel"></param>
/// <param name="renewal"></param>
- internal void NotifySuccess(Renewal renewal, IEnumerable<MemoryEntry> log)
+ internal async Task NotifySuccess(Renewal renewal, IEnumerable<MemoryEntry> log)
{
// Do not send emails when running interactively
_log.Information(
@@ -65,7 +66,7 @@ namespace PKISharp.WACS.Services
renewal.LastFriendlyName);
if (_settings.Notification.EmailOnSuccess)
{
- _email.Send(
+ await _email.Send(
$"Certificate renewal {renewal.LastFriendlyName} completed",
@$"<p>Certificate <b>{HttpUtility.HtmlEncode(renewal.LastFriendlyName)}</b> succesfully renewed.</p>
{NotificationInformation(renewal)}
@@ -79,7 +80,7 @@ namespace PKISharp.WACS.Services
/// </summary>
/// <param name="runLevel"></param>
/// <param name="renewal"></param>
- internal void NotifyFailure(
+ internal async Task NotifyFailure(
RunLevel runLevel,
Renewal renewal,
List<string> errors,
@@ -93,7 +94,7 @@ namespace PKISharp.WACS.Services
}
if (runLevel.HasFlag(RunLevel.Unattended))
{
- _email.Send(
+ await _email.Send(
$"Error processing certificate renewal {renewal.LastFriendlyName}",
@$"<p>Renewal for <b>{HttpUtility.HtmlEncode(renewal.LastFriendlyName)}</b> failed, will retry on next run.<br><br>Error(s):
<ul><li>{string.Join("</li><li>", errors.Select(x => HttpUtility.HtmlEncode(x)))}</li></ul></p>
diff --git a/src/main.lib/Services/PluginService.cs b/src/main.lib/Services/PluginService.cs
index 023f0f0..af06227 100644
--- a/src/main.lib/Services/PluginService.cs
+++ b/src/main.lib/Services/PluginService.cs
@@ -107,9 +107,9 @@ namespace PKISharp.WACS.Services
{
return x.AsType();
}
- catch (Exception)
+ catch (Exception ex)
{
- _log.Error("Error loading type {x}", x.FullName);
+ _log.Error(ex, "Error loading type {x}", x.FullName);
throw;
}
}
diff --git a/src/main.lib/Services/SettingsService.cs b/src/main.lib/Services/SettingsService.cs
index ab03b4e..e930c96 100644
--- a/src/main.lib/Services/SettingsService.cs
+++ b/src/main.lib/Services/SettingsService.cs
@@ -485,6 +485,11 @@ namespace PKISharp.WACS.Services
public string? DefaultValidationMode { get; set; }
/// <summary>
+ /// Disable multithreading for validation
+ /// </summary>
+ public bool? DisableMultiThreading { get; set; }
+
+ /// <summary>
/// If set to True, it will cleanup the folder structure
/// and files it creates under the site for authorization.
/// </summary>
diff --git a/src/main.lib/Services/TaskSchedulerService.cs b/src/main.lib/Services/TaskSchedulerService.cs
index 37f3c0f..28ccf81 100644
--- a/src/main.lib/Services/TaskSchedulerService.cs
+++ b/src/main.lib/Services/TaskSchedulerService.cs
@@ -65,11 +65,11 @@ namespace PKISharp.WACS.Services
{
var healthy = true;
if (!task.Definition.Actions.OfType<ExecAction>().Any(action =>
- action.Path == _settings.ExePath &&
- action.WorkingDirectory == WorkingDirectory))
+ string.Equals(action.Path, _settings.ExePath, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(action.WorkingDirectory, WorkingDirectory, StringComparison.OrdinalIgnoreCase)))
{
healthy = false;
- _log.Warning("Scheduled task points to different location");
+ _log.Warning("Scheduled task points to different location for .exe and/or working directory");
}
if (!task.Enabled)
{
diff --git a/src/main.test/Mock/MockContainer.cs b/src/main.test/Mock/MockContainer.cs
index 1e06ebb..76560ae 100644
--- a/src/main.test/Mock/MockContainer.cs
+++ b/src/main.test/Mock/MockContainer.cs
@@ -57,6 +57,7 @@ namespace PKISharp.WACS.UnitTests.Mock
_ = builder.RegisterType<mock.CertificateService>().As<real.ICertificateService>().SingleInstance();
_ = builder.RegisterType<real.TaskSchedulerService>().SingleInstance();
_ = builder.RegisterType<real.NotificationService>().SingleInstance();
+ _ = builder.RegisterType<RenewalValidator>().SingleInstance();
_ = builder.RegisterType<RenewalExecutor>().SingleInstance();
_ = builder.RegisterType<RenewalManager>().SingleInstance();
_ = builder.Register(c => c.Resolve<real.IArgumentsService>().MainArguments).SingleInstance();
diff --git a/src/main.test/Tests/BindingTests/Bindings.cs b/src/main.test/Tests/BindingTests/Bindings.cs
index 8b4a57a..17c886d 100644
--- a/src/main.test/Tests/BindingTests/Bindings.cs
+++ b/src/main.test/Tests/BindingTests/Bindings.cs
@@ -882,6 +882,41 @@ namespace PKISharp.WACS.UnitTests.Tests.BindingTests
Assert.AreEqual(oldCert1, updatedBinding.CertificateHash);
}
+ /// <summary>
+ /// Like above, but SNI cannot be turned on for the default
+ /// website / empty host. The code should ignore the change.
+ /// </summary>
+ [TestMethod]
+ public void CentralSSLTrap()
+ {
+ var iis = new MockIISClient(log)
+ {
+ MockSites = new[] {
+ new MockSite() {
+ Id = 1,
+ Bindings = new List<MockBinding> {
+ new MockBinding() {
+ IP = DefaultIP,
+ Port = DefaultPort,
+ Host = "",
+ Protocol = "http"
+ }
+ }
+ }
+ }
+ };
+
+ var bindingOptions = new BindingOptions().
+ WithSiteId(1).
+ WithIP(DefaultIP).
+ WithPort(DefaultPort).
+ WithFlags(SSLFlags.CentralSsl);
+
+ iis.AddOrUpdateBindings(new[] { "mail.example.com" }, bindingOptions, null);
+
+ Assert.IsTrue(true);
+ }
+
[TestMethod]
public void DuplicateBinding()
{
diff --git a/src/main.test/Tests/RenewalTests/RenewalServiceTests.cs b/src/main.test/Tests/RenewalTests/RenewalServiceTests.cs
index 82b805a..6c17610 100644
--- a/src/main.test/Tests/RenewalTests/RenewalServiceTests.cs
+++ b/src/main.test/Tests/RenewalTests/RenewalServiceTests.cs
@@ -22,8 +22,11 @@ namespace PKISharp.WACS.UnitTests.Tests.RenewalTests
{
var container = new MockContainer().TestScope();
var renewalStore = container.Resolve<real.IRenewalStore>();
+ var renewalValidator = container.Resolve<RenewalValidator>(
+ new TypedParameter(typeof(IContainer), container));
var renewalExecutor = container.Resolve<RenewalExecutor>(
- new TypedParameter(typeof(IContainer), container));
+ new TypedParameter(typeof(RenewalValidator), renewalValidator),
+ new TypedParameter(typeof(IContainer), container));
var renewalManager = container.Resolve<RenewalManager>(
new TypedParameter(typeof(IContainer), container),
new TypedParameter(typeof(RenewalExecutor), renewalExecutor));
diff --git a/src/main/Program.cs b/src/main/Program.cs
index 4811c3b..e02a212 100644
--- a/src/main/Program.cs
+++ b/src/main/Program.cs
@@ -126,6 +126,7 @@ namespace PKISharp.WACS.Host
_ = builder.RegisterType<TaskSchedulerService>().SingleInstance();
_ = builder.RegisterType<NotificationService>().SingleInstance();
_ = builder.RegisterType<RenewalExecutor>().SingleInstance();
+ _ = builder.RegisterType<RenewalValidator>().SingleInstance();
_ = builder.RegisterType<RenewalManager>().SingleInstance();
_ = builder.RegisterType<RenewalCreator>().SingleInstance();
_ = builder.Register(c => c.Resolve<IArgumentsService>().MainArguments).SingleInstance();
diff --git a/src/main/settings.json b/src/main/settings.json
index 54d61c4..d40bd2d 100644
--- a/src/main/settings.json
+++ b/src/main/settings.json
@@ -61,6 +61,7 @@
"Validation": {
"DefaultValidation": null,
"DefaultValidationMode": null,
+ "DisableMultiThreading": true,
"CleanupFolders": true,
"PreValidateDns": true,
"PreValidateDnsRetryCount": 5,
@@ -94,4 +95,4 @@
"Installation": {
"DefaultInstallation": null
}
-} \ No newline at end of file
+}
diff --git a/src/plugin.validation.dns.azure/Azure.cs b/src/plugin.validation.dns.azure/Azure.cs
index 2d8c1e0..2a0c741 100755
--- a/src/plugin.validation.dns.azure/Azure.cs
+++ b/src/plugin.validation.dns.azure/Azure.cs
@@ -4,68 +4,148 @@ using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Rest;
using Microsoft.Rest.Azure.Authentication;
using PKISharp.WACS.Clients.DNS;
+using PKISharp.WACS.Plugins.Interfaces;
using PKISharp.WACS.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-
namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
{
+ /// <summary>
+ /// Handle creation of DNS records in Azure
+ /// </summary>
internal class Azure : DnsValidation<Azure>
{
private DnsManagementClient _azureDnsClient;
- private readonly DomainParseService _domainParser;
private readonly ProxyService _proxyService;
-
private readonly AzureOptions _options;
+ private readonly Dictionary<string, Dictionary<string, RecordSet>> _recordSets;
+ private IEnumerable<Zone> _hostedZones;
+
public Azure(AzureOptions options,
- DomainParseService domainParser,
LookupClientProvider dnsClient,
ProxyService proxyService,
ILogService log,
- ISettingsService settings)
- : base(dnsClient, log, settings)
+ ISettingsService settings) : base(dnsClient, log, settings)
{
_options = options;
- _domainParser = domainParser;
_proxyService = proxyService;
+ _recordSets = new Dictionary<string, Dictionary<string, RecordSet>>();
}
- public override async Task<bool> CreateRecord(string recordName, string token)
+ /// <summary>
+ /// Allow this plugin to process multiple validations at the same time.
+ /// They will still be prepared and cleaned in serial order though not
+ /// to overwhelm the DnsManagementClient or risk threads overwriting
+ /// eachothers changes.
+ /// </summary>
+ public override ParallelOperations Parallelism => ParallelOperations.Answer;
+
+ /// <summary>
+ /// Create record in Azure DNS
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="recordName"></param>
+ /// <param name="token"></param>
+ /// <returns></returns>
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
{
- var client = await GetClient();
- var zone = await GetHostedZone(recordName);
+ var zone = await GetHostedZone(record.Authority.Domain);
if (zone == null)
{
return false;
}
+ // Create or update record set parameters
+ var txtRecord = new TxtRecord(new[] { record.Value });
+ if (!_recordSets.ContainsKey(zone))
+ {
+ _recordSets.Add(zone, new Dictionary<string, RecordSet>());
+ }
+ var zoneRecords = _recordSets[zone];
+ var relativeKey = RelativeRecordName(zone, record.Authority.Domain);
+ if (!zoneRecords.ContainsKey(relativeKey))
+ {
+ zoneRecords.Add(
+ relativeKey,
+ new RecordSet
+ {
+ TTL = 0,
+ TxtRecords = new List<TxtRecord> { txtRecord }
+ });
+ }
+ else
+ {
+ zoneRecords[relativeKey].TxtRecords.Add(txtRecord);
+ }
+ return true;
+ }
- // Create record set parameters
- var recordSetParams = new RecordSet
+ /// <summary>
+ /// Send all buffered changes to Azure
+ /// </summary>
+ /// <returns></returns>
+ public override async Task SaveChanges()
+ {
+ var updateTasks = new List<Task>();
+ foreach (var zone in _recordSets.Keys)
{
- TTL = 0,
- TxtRecords = new List<TxtRecord>
+ foreach (var domain in _recordSets[zone].Keys)
{
- new TxtRecord(new[] { token })
+ updateTasks.Add(CreateOrUpdateRecordSet(zone, domain));
}
- };
+ }
+ await Task.WhenAll(updateTasks);
+ }
+ /// <summary>
+ /// Store a single recordset
+ /// </summary>
+ /// <param name="zone"></param>
+ /// <param name="domain"></param>
+ /// <param name="recordSet"></param>
+ /// <returns></returns>
+ private async Task CreateOrUpdateRecordSet(string zone, string domain)
+ {
try
{
- _ = await client.RecordSets.CreateOrUpdateAsync(_options.ResourceGroupName,
- zone,
- RelativeRecordName(zone, recordName),
- RecordType.TXT,
- recordSetParams);
+ var newSet = _recordSets[zone][domain];
+ var client = await GetClient();
+ try
+ {
+ var originalSet = await client.RecordSets.GetAsync(_options.ResourceGroupName,
+ zone,
+ domain,
+ RecordType.TXT);
+ _recordSets[zone][domain] = originalSet;
+ }
+ catch
+ {
+ _recordSets[zone][domain] = null;
+ }
+ if (newSet == null)
+ {
+ await client.RecordSets.DeleteAsync(
+ _options.ResourceGroupName,
+ zone,
+ domain,
+ RecordType.TXT);
+ }
+ else
+ {
+ _ = await client.RecordSets.CreateOrUpdateAsync(
+ _options.ResourceGroupName,
+ zone,
+ domain,
+ RecordType.TXT,
+ newSet);
+ }
}
catch (Exception ex)
{
- _log.Error(ex, "Error updating record in Azure");
- return false;
+ _log.Error(ex, "Error updating DNS records in {zone} ({domain})", zone, domain);
}
- return true;
}
private async Task<DnsManagementClient> GetClient()
@@ -97,26 +177,42 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
}
return _azureDnsClient;
}
+
+ /// <summary>
+ /// Translate full host name to zone relative name
+ /// </summary>
+ /// <param name="zone"></param>
+ /// <param name="recordName"></param>
+ /// <returns></returns>
private string RelativeRecordName(string zone, string recordName)
{
var ret = recordName.Substring(0, recordName.LastIndexOf(zone)).TrimEnd('.');
return string.IsNullOrEmpty(ret) ? "@" : ret;
}
-
+ /// <summary>
+ /// Find the approriate hosting zone to use for record updates
+ /// </summary>
+ /// <param name="recordName"></param>
+ /// <returns></returns>
private async Task<string> GetHostedZone(string recordName)
{
- var client = await GetClient();
- var zones = new List<Zone>();
- var response = await client.Zones.ListByResourceGroupAsync(_options.ResourceGroupName);
- zones.AddRange(response);
- while (!string.IsNullOrEmpty(response.NextPageLink))
+ // Cache so we don't have to repeat this more than once for each renewal
+ if (_hostedZones == null)
{
- response = await client.Zones.ListByResourceGroupNextAsync(response.NextPageLink);
+ var client = await GetClient();
+ var zones = new List<Zone>();
+ var response = await client.Zones.ListByResourceGroupAsync(_options.ResourceGroupName);
+ zones.AddRange(response);
+ while (!string.IsNullOrEmpty(response.NextPageLink))
+ {
+ response = await client.Zones.ListByResourceGroupNextAsync(response.NextPageLink);
+ }
+ _log.Debug("Found {count} hosted zones in Azure Resource Group {rg}", zones.Count, _options.ResourceGroupName);
+ _hostedZones = zones;
}
- _log.Debug("Found {count} hosted zones in Azure Resource Group {rg}", zones, _options.ResourceGroupName);
- var hostedZone = FindBestMatch(zones.ToDictionary(x => x.Name), recordName);
+ var hostedZone = FindBestMatch(_hostedZones.ToDictionary(x => x.Name), recordName);
if (hostedZone != null)
{
return hostedZone.Name;
@@ -128,22 +224,19 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
return null;
}
- public override async Task DeleteRecord(string recordName, string token)
- {
- var client = await GetClient();
- var zone = await GetHostedZone(recordName);
- try
- {
- await client.RecordSets.DeleteAsync(
- _options.ResourceGroupName,
- zone,
- RelativeRecordName(zone, recordName),
- RecordType.TXT);
- }
- catch (Exception ex)
- {
- _log.Error(ex, "Error deleting record from Azure");
- }
- }
+ /// <summary>
+ /// Ignored because we keep track of our list of changes
+ /// </summary>
+ /// <param name="record"></param>
+ /// <returns></returns>
+ public override Task DeleteRecord(DnsValidationRecord record) => Task.CompletedTask;
+
+ /// <summary>
+ /// Clear created createds
+ /// </summary>
+ /// <returns></returns>
+ public override async Task Finalize() =>
+ // We save the original record sets, so this should restore them
+ await SaveChanges();
}
}
diff --git a/src/plugin.validation.dns.cloudflare/Cloudflare.cs b/src/plugin.validation.dns.cloudflare/Cloudflare.cs
index e770735..98c8cbf 100644
--- a/src/plugin.validation.dns.cloudflare/Cloudflare.cs
+++ b/src/plugin.validation.dns.cloudflare/Cloudflare.cs
@@ -3,6 +3,7 @@ using FluentCloudflare.Api;
using FluentCloudflare.Api.Entities;
using FluentCloudflare.Extensions;
using PKISharp.WACS.Clients.DNS;
+using PKISharp.WACS.Context;
using PKISharp.WACS.Services;
using System;
using System.Linq;
@@ -23,8 +24,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
ProxyService proxyService,
LookupClientProvider dnsClient,
ILogService log,
- ISettingsService settings)
- : base(dnsClient, log, settings)
+ ISettingsService settings) : base(dnsClient, log, settings)
{
_options = options;
_hc = proxyService.GetHttpClient();
@@ -54,10 +54,10 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
return zonesResp.Unpack().First();
}
- public override async Task<bool> CreateRecord(string recordName, string token)
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
{
var ctx = GetContext();
- var zone = await GetHostedZone(ctx, recordName).ConfigureAwait(false);
+ var zone = await GetHostedZone(ctx, record.Authority.Domain).ConfigureAwait(false);
if (zone == null)
{
_log.Error("The zone could not be found using the Cloudflare API, thus creating a DNS validation record is impossible. " +
@@ -68,7 +68,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
}
var dns = ctx.Zone(zone).Dns;
- _ = await dns.Create(DnsRecordType.TXT, recordName, token)
+ _ = await dns.Create(DnsRecordType.TXT, record.Authority.Domain, record.Value)
.CallAsync(_hc)
.ConfigureAwait(false);
return true;
@@ -105,11 +105,11 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
}
- public override async Task DeleteRecord(string recordName, string token)
+ public override async Task DeleteRecord(DnsValidationRecord record)
{
var ctx = GetContext();
- var zone = await GetHostedZone(ctx, recordName).ConfigureAwait(false);
- await DeleteRecord(recordName, token, ctx, zone);
+ var zone = await GetHostedZone(ctx, record.Authority.Domain).ConfigureAwait(false);
+ await DeleteRecord(record.Authority.Domain, record.Value, ctx, zone);
}
public void Dispose() => _hc.Dispose();
diff --git a/src/plugin.validation.dns.dreamhost/DreamhostDnsValidation.cs b/src/plugin.validation.dns.dreamhost/DreamhostDnsValidation.cs
index 2e6e5db..2638d63 100644
--- a/src/plugin.validation.dns.dreamhost/DreamhostDnsValidation.cs
+++ b/src/plugin.validation.dns.dreamhost/DreamhostDnsValidation.cs
@@ -1,4 +1,5 @@
using PKISharp.WACS.Clients.DNS;
+using PKISharp.WACS.Context;
using PKISharp.WACS.Plugins.ValidationPlugins.Dreamhost;
using PKISharp.WACS.Services;
using System;
@@ -18,11 +19,11 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
: base(dnsClient, logService, settings)
=> _client = new DnsManagementClient(options.ApiKey.Value, logService);
- public override async Task<bool> CreateRecord(string recordName, string token)
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
{
try
{
- await _client.CreateRecord(recordName, RecordType.TXT, token);
+ await _client.CreateRecord(record.Authority.Domain, RecordType.TXT, record.Value);
return true;
}
catch
@@ -31,11 +32,11 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
}
}
- public override async Task DeleteRecord(string recordName, string token)
+ public override async Task DeleteRecord(DnsValidationRecord record)
{
try
{
- await _client.DeleteRecord(recordName, RecordType.TXT, token);
+ await _client.DeleteRecord(record.Authority.Domain, RecordType.TXT, record.Value);
}
catch (Exception ex)
{
diff --git a/src/plugin.validation.dns.luadns/luadns.cs b/src/plugin.validation.dns.luadns/luadns.cs
index 4935681..c95a2bf 100644
--- a/src/plugin.validation.dns.luadns/luadns.cs
+++ b/src/plugin.validation.dns.luadns/luadns.cs
@@ -1,4 +1,5 @@
using PKISharp.WACS.Clients.DNS;
+using PKISharp.WACS.Context;
using PKISharp.WACS.Services;
using System;
using System.Collections.Generic;
@@ -64,7 +65,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
_apiKey = options.APIKey.Value;
}
- public override async Task<bool> CreateRecord(string recordName, string token)
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
{
_log.Information("Creating LuaDNS verification record");
@@ -78,14 +79,14 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
var payload = await response.Content.ReadAsStringAsync();
var zones = JsonSerializer.Deserialize<ZoneData[]>(payload);
- var targetZone = FindBestMatch(zones.ToDictionary(x => x.Name), recordName);
+ var targetZone = FindBestMatch(zones.ToDictionary(x => x.Name), record.Authority.Domain);
if (targetZone == null)
{
_log.Error("No matching zone found in LuaDNS account. Aborting");
return false;
}
- var newRecord = new RecordData { Name = $"{recordName}.", Type = "TXT", Content = token, TTL = 300 };
+ var newRecord = new RecordData { Name = $"{record.Authority.Domain}.", Type = "TXT", Content = record.Value, TTL = 300 };
payload = JsonSerializer.Serialize(newRecord);
response = await client.PostAsync(new Uri(_LuaDnsApiEndpoint, $"zones/{targetZone.Id}/records"), new StringContent(payload, Encoding.UTF8, "application/json"));
@@ -97,29 +98,30 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
payload = await response.Content.ReadAsStringAsync();
newRecord = JsonSerializer.Deserialize<RecordData>(payload);
- _recordsMap[recordName] = newRecord;
+ _recordsMap[record.Authority.Domain] = newRecord;
return true;
}
- public override async Task DeleteRecord(string recordName, string token)
+ public override async Task DeleteRecord(DnsValidationRecord record)
{
- if (!_recordsMap.ContainsKey(recordName))
+ if (!_recordsMap.ContainsKey(record.Authority.Domain))
{
- _log.Warning($"No record with name {recordName} was created");
+ _log.Warning($"No record with name {record.Authority.Domain} was created");
return;
}
_log.Information("Deleting LuaDNS verification record");
using var client = GetClient();
- var response = await client.DeleteAsync(new Uri(_LuaDnsApiEndpoint, $"zones/{_recordsMap[recordName].ZoneId}/records/{_recordsMap[recordName].Id}"));
+ var created = _recordsMap[record.Authority.Domain];
+ var response = await client.DeleteAsync(new Uri(_LuaDnsApiEndpoint, $"zones/{created.ZoneId}/records/{created.Id}"));
if (!response.IsSuccessStatusCode)
{
_log.Warning("Failed to delete DNS verification record");
return;
}
- _ = _recordsMap.Remove(recordName);
+ _ = _recordsMap.Remove(record.Authority.Domain);
}
private HttpClient GetClient()
diff --git a/src/plugin.validation.dns.route53/Route53.cs b/src/plugin.validation.dns.route53/Route53.cs
index a2d490c..38aae55 100644
--- a/src/plugin.validation.dns.route53/Route53.cs
+++ b/src/plugin.validation.dns.route53/Route53.cs
@@ -1,10 +1,11 @@
-using System;
-using Amazon;
+using Amazon;
using Amazon.Route53;
using Amazon.Route53.Model;
using Amazon.Runtime;
using PKISharp.WACS.Clients.DNS;
+using PKISharp.WACS.Context;
using PKISharp.WACS.Services;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -14,16 +15,13 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
internal sealed class Route53 : DnsValidation<Route53>
{
private readonly IAmazonRoute53 _route53Client;
- private readonly DomainParseService _domainParser;
public Route53(
LookupClientProvider dnsClient,
- DomainParseService domainParser,
ILogService log,
ProxyService proxy,
ISettingsService settings,
- Route53Options options)
- : base(dnsClient, log, settings)
+ Route53Options options) : base(dnsClient, log, settings)
{
var region = RegionEndpoint.USEast1;
var config = new AmazonRoute53Config() { RegionEndpoint = region };
@@ -33,7 +31,6 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
: !string.IsNullOrWhiteSpace(options.AccessKeyId) && !string.IsNullOrWhiteSpace(options.SecretAccessKey.Value)
? new AmazonRoute53Client(options.AccessKeyId, options.SecretAccessKey.Value, config)
: new AmazonRoute53Client(config);
- _domainParser = domainParser;
}
private static ResourceRecordSet CreateResourceRecordSet(string name, string value)
@@ -48,25 +45,30 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
};
}
- public override async Task<bool> CreateRecord(string recordName, string token)
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
{
try
{
- var hostedZoneId = await GetHostedZoneId(recordName);
- if (hostedZoneId == null)
+ var recordName = record.Authority.Domain;
+ var token = record.Value;
+ var hostedZoneIds = await GetHostedZoneIds(recordName);
+ if (hostedZoneIds == null)
{
return false;
}
_log.Information("Creating TXT record {recordName} with value {token}", recordName, token);
- var response = await _route53Client.ChangeResourceRecordSetsAsync(
- new ChangeResourceRecordSetsRequest(
- hostedZoneId,
- new ChangeBatch(new List<Change> {
- new Change(
- ChangeAction.UPSERT,
- CreateResourceRecordSet(recordName, token))
- })));
- await WaitChangesPropagation(response.ChangeInfo);
+ var updateTasks = hostedZoneIds.Select(hostedZoneId =>
+ _route53Client.ChangeResourceRecordSetsAsync(
+ new ChangeResourceRecordSetsRequest(
+ hostedZoneId,
+ new ChangeBatch(new List<Change> {
+ new Change(
+ ChangeAction.UPSERT,
+ CreateResourceRecordSet(recordName, token))
+ }))));
+ var results = await Task.WhenAll(updateTasks);
+ var propagationTasks = results.Select(result => WaitChangesPropagation(result.ChangeInfo));
+ await Task.WhenAll(propagationTasks);
return true;
}
catch (Exception ex)
@@ -76,20 +78,24 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
}
}
- public override async Task DeleteRecord(string recordName, string token)
+ public override async Task DeleteRecord(DnsValidationRecord record)
{
- var hostedZoneId = await GetHostedZoneId(recordName);
+ var recordName = record.Authority.Domain;
+ var token = record.Value;
+ var hostedZoneIds = await GetHostedZoneIds(recordName);
_log.Information($"Deleting TXT record {recordName} with value {token}");
- _ = await _route53Client.ChangeResourceRecordSetsAsync(
- new ChangeResourceRecordSetsRequest(hostedZoneId,
- new ChangeBatch(new List<Change> {
+ var deleteTasks = hostedZoneIds.Select(hostedZoneId =>
+ _route53Client.ChangeResourceRecordSetsAsync(
+ new ChangeResourceRecordSetsRequest(hostedZoneId,
+ new ChangeBatch(new List<Change> {
new Change(
ChangeAction.DELETE,
CreateResourceRecordSet(recordName, token))
- })));
+ }))));
+ _ = await Task.WhenAll(deleteTasks);
}
- private async Task<string> GetHostedZoneId(string recordName)
+ private async Task<IEnumerable<string>> GetHostedZoneIds(string recordName)
{
var hostedZones = new List<HostedZone>();
var response = await _route53Client.ListHostedZonesAsync();
@@ -104,10 +110,12 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
}
_log.Debug("Found {count} hosted zones in AWS", hostedZones);
- var hostedZone = FindBestMatch(hostedZones.ToDictionary(x => x.Name), recordName);
+ hostedZones = hostedZones.Where(x => !x.Config.PrivateZone).ToList();
+ var hostedZoneSets = hostedZones.GroupBy(x => x.Name);
+ var hostedZone = FindBestMatch(hostedZoneSets.ToDictionary(x => x.Key), recordName);
if (hostedZone != null)
{
- return hostedZone.Id;
+ return hostedZone.Select(x => x.Id);
}
_log.Error($"Can't find hosted zone for domain {recordName}");
return null;