summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHank McCord <Henry.McCord@ewudn.robins.af.mil>2020-07-06 12:57:53 -0400
committerHank McCord <Henry.McCord@ewudn.robins.af.mil>2020-07-06 13:05:08 -0400
commit789cda2aa2cf2343c7ac7d3923ec12fe2ba1889d (patch)
treed34e99666f655790f95268b593ba995b13c23662
parent7f3c13e454eff5a3c39d4b20ae662e924baec35a (diff)
parent25e4ebdadf35a0050eeeacf3cf607fad8ab8a641 (diff)
downloadletsencrypt-win-simple-789cda2aa2cf2343c7ac7d3923ec12fe2ba1889d.zip
letsencrypt-win-simple-789cda2aa2cf2343c7ac7d3923ec12fe2ba1889d.tar.gz
letsencrypt-win-simple-789cda2aa2cf2343c7ac7d3923ec12fe2ba1889d.tar.bz2
Merge branch 2.1.9 into azure-environment-agnostic
-rw-r--r--.github/FUNDING.yml1
-rw-r--r--.github/workflows/dotnetcore.yml16
-rw-r--r--appveyor.yml3
-rw-r--r--build/create-artifacts.ps14
-rw-r--r--dist/Scripts/ImportADFS.ps173
m---------src/ACMESharpCore0
-rw-r--r--src/main.lib/Clients/Acme/AcmeClient.cs1156
-rw-r--r--src/main.lib/Clients/Acme/OrderManager.cs386
-rw-r--r--src/main.lib/Clients/AcmeDnsClient.cs36
-rw-r--r--src/main.lib/Clients/DNS/LookupClientProvider.cs56
-rw-r--r--src/main.lib/Clients/DNS/LookupClientWrapper.cs61
-rw-r--r--src/main.lib/Clients/EmailClient.cs27
-rw-r--r--src/main.lib/Clients/IIS/BindingOptions.cs25
-rw-r--r--src/main.lib/Clients/IIS/IISClient.cs13
-rw-r--r--src/main.lib/Clients/ScriptClient.cs2
-rw-r--r--src/main.lib/Configuration/BaseArgumentsProvider.cs18
-rw-r--r--src/main.lib/Configuration/NetworkCredentialOptions.cs8
-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/CsrPlugins/Ec/EcOptionsFactory.cs4
-rw-r--r--src/main.lib/Plugins/CsrPlugins/Rsa/RsaOptionsFactory.cs4
-rw-r--r--src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtpOptionsFactory.cs2
-rw-r--r--src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebArgumentsProvider.cs10
-rw-r--r--src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebOptions.cs6
-rw-r--r--src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebOptionsFactory.cs2
-rw-r--r--src/main.lib/Plugins/InstallationPlugins/Script/ScriptOptionsFactory.cs15
-rw-r--r--src/main.lib/Plugins/Interfaces/IValidationPlugin.cs28
-rw-r--r--src/main.lib/Plugins/OrderPlugins/Domain/Domain.cs64
-rw-r--r--src/main.lib/Plugins/OrderPlugins/Domain/DomainOptions.cs12
-rw-r--r--src/main.lib/Plugins/OrderPlugins/Domain/DomainOptionsFactory.cs14
-rw-r--r--src/main.lib/Plugins/OrderPlugins/Host/Host.cs2
-rw-r--r--src/main.lib/Plugins/OrderPlugins/Host/HostOptionsFactory.cs2
-rw-r--r--src/main.lib/Plugins/OrderPlugins/Site/Site.cs28
-rw-r--r--src/main.lib/Plugins/OrderPlugins/Site/SiteOptions.cs12
-rw-r--r--src/main.lib/Plugins/OrderPlugins/Site/SiteOptionsFactory.cs14
-rw-r--r--src/main.lib/Plugins/Resolvers/InteractiveResolver.cs29
-rw-r--r--src/main.lib/Plugins/Resolvers/UnattendedResolver.cs2
-rw-r--r--src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs40
-rw-r--r--src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptions.cs2
-rw-r--r--src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptionsFactory.cs26
-rw-r--r--src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStore.cs6
-rw-r--r--src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptionsFactory.cs6
-rw-r--r--src/main.lib/Plugins/StorePlugins/PemFiles/PemFiles.cs39
-rw-r--r--src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptionsFactory.cs10
-rw-r--r--src/main.lib/Plugins/StorePlugins/PfxFile/PfxFile.cs73
-rw-r--r--src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileArguments.cs8
-rw-r--r--src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileArgumentsProvider.cs22
-rw-r--r--src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptions.cs36
-rw-r--r--src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptionsFactory.cs91
-rw-r--r--src/main.lib/Plugins/TargetPlugins/Csr/CsrOptionsFactory.cs14
-rw-r--r--src/main.lib/Plugins/TargetPlugins/IIS/IISOptions.cs2
-rw-r--r--src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs135
-rw-r--r--src/main.lib/Plugins/TargetPlugins/Manual/ManualOptionsFactory.cs4
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Dns/Acme/Acme.cs12
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Dns/Acme/AcmeOptionsFactory.cs2
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs426
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Dns/Manual/Manual.cs40
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Dns/Script/Script.cs55
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Dns/Script/ScriptOptionsFactory.cs12
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs18
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemOptionsFactory.cs2
-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/HttpValidationOptionsFactory.cs4
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationParameters.cs8
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHosting.cs84
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptionsFactory.cs4
-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/Tls/SelfHosting/SelfHostingOptionsFactory.cs2
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Validation.cs35
-rw-r--r--src/main.lib/RenewalCreator.cs20
-rw-r--r--src/main.lib/RenewalExecutor.cs244
-rw-r--r--src/main.lib/RenewalManager.cs39
-rw-r--r--src/main.lib/RenewalValidator.cs446
-rw-r--r--src/main.lib/Services/ArgumentsParser.cs12
-rw-r--r--src/main.lib/Services/ArgumentsService.cs6
-rw-r--r--src/main.lib/Services/AutofacBuilder.cs11
-rw-r--r--src/main.lib/Services/CertificateService.cs1065
-rw-r--r--src/main.lib/Services/DomainParseService.cs4
-rw-r--r--src/main.lib/Services/InputService.cs29
-rw-r--r--src/main.lib/Services/Interfaces/IArgumentsProvider.cs15
-rw-r--r--src/main.lib/Services/Interfaces/IArgumentsService.cs2
-rw-r--r--src/main.lib/Services/Interfaces/IAutofacBuilder.cs2
-rw-r--r--src/main.lib/Services/Interfaces/IInputService.cs3
-rw-r--r--src/main.lib/Services/Interfaces/ILogService.cs6
-rw-r--r--src/main.lib/Services/Legacy/Importer.cs4
-rw-r--r--src/main.lib/Services/Log/LogService.cs (renamed from src/main.lib/Services/LogService.cs)20
-rw-r--r--src/main.lib/Services/Log/MemorySink.cs46
-rw-r--r--src/main.lib/Services/NotificationService.cs111
-rw-r--r--src/main.lib/Services/PluginService.cs10
-rw-r--r--src/main.lib/Services/ProxyService.cs19
-rw-r--r--src/main.lib/Services/RenewalStoreDisk.cs34
-rw-r--r--src/main.lib/Services/SettingsService.cs80
-rw-r--r--src/main.lib/Services/TaskSchedulerService.cs6
-rw-r--r--src/main.lib/Wacs.cs19
-rw-r--r--src/main.lib/wacs.lib.csproj24
-rw-r--r--src/main.test/Mock/MockContainer.cs68
-rw-r--r--src/main.test/Mock/Services/InputService.cs8
-rw-r--r--src/main.test/Mock/Services/LogService.cs6
-rw-r--r--src/main.test/Tests/BindingTests/Bindings.cs35
-rw-r--r--src/main.test/Tests/DnsValidationTests/When_resolving_name_servers.cs14
-rw-r--r--src/main.test/Tests/EncryptionTests/EncryptionTest.cs (renamed from src/main.test/Tests/EcnryptionTests/EncryptionTest.cs)0
-rw-r--r--src/main.test/Tests/InstallationPluginTests/ArgumentParserTests.cs6
-rw-r--r--src/main.test/Tests/OrderPluginTests/DomainPluginTests.cs24
-rw-r--r--src/main.test/Tests/RenewalTests/RenewalServiceTests.cs54
-rw-r--r--src/main.test/wacs.test.csproj8
-rw-r--r--src/main/Program.cs3
-rw-r--r--src/main/settings.json23
-rw-r--r--src/plugin.validation.dns.azure/Azure.cs222
-rwxr-xr-xsrc/plugin.validation.dns.azure/wacs.validation.dns.azure.csproj2
-rw-r--r--src/plugin.validation.dns.cloudflare/Cloudflare.cs59
-rw-r--r--src/plugin.validation.dns.dreamhost/DreamhostArgumentsProvider.cs2
-rw-r--r--src/plugin.validation.dns.dreamhost/DreamhostDnsValidation.cs27
-rw-r--r--src/plugin.validation.dns.luadns/luadns.cs106
-rw-r--r--src/plugin.validation.dns.luadns/luadnsArguments.cs6
-rw-r--r--src/plugin.validation.dns.luadns/luadnsArgumentsProvider.cs20
-rw-r--r--src/plugin.validation.dns.luadns/luadnsOptions.cs6
-rw-r--r--src/plugin.validation.dns.luadns/luadnsOptionsFactory.cs24
-rw-r--r--src/plugin.validation.dns.luadns/wacs.validation.dns.luadns.csproj2
-rw-r--r--src/plugin.validation.dns.route53/Route53.cs111
-rw-r--r--src/plugin.validation.dns.route53/wacs.validation.dns.route53.csproj2
123 files changed, 4246 insertions, 2581 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 46b5c83..3f21049 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,4 +1,5 @@
# These are supported funding model platforms
+github: [WouterTinus]
patreon: WouterTinus
custom: ["https://www.paypal.me/woutertinus"]
diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml
deleted file mode 100644
index ab6c6c8..0000000
--- a/.github/workflows/dotnetcore.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-name: .NET Core
-
-on: [pull_request]
-
-jobs:
- build:
- runs-on: windows-latest
-
- steps:
- - uses: actions/checkout@v2
- - name: Setup .NET Core
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: 3.1
- - name: Build with dotnet
- run: dotnet build --configuration Release
diff --git a/appveyor.yml b/appveyor.yml
index 7522c7e..ff2cc11 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 2.1.7.{build}
+version: 2.1.9.{build}
image: Visual Studio 2019
platform: Any CPU
shallow_clone: true
@@ -27,6 +27,7 @@ build_script:
- cmd: dotnet publish ./src/plugin.validation.dns.azure/wacs.validation.dns.azure.csproj -c Release
- cmd: dotnet publish ./src/plugin.validation.dns.dreamhost/wacs.validation.dns.dreamhost.csproj -c Release
- cmd: dotnet publish ./src/plugin.validation.dns.route53/wacs.validation.dns.route53.csproj -c Release
+ - cmd: dotnet publish ./src/plugin.validation.dns.luadns/wacs.validation.dns.luadns.csproj -c Release
- cmd: dotnet publish ./src/plugin.validation.dns.cloudflare/wacs.validation.dns.cloudflare.csproj -c Release
test_script:
- cmd: cd %APPVEYOR_BUILD_FOLDER%/src/main.test/
diff --git a/build/create-artifacts.ps1 b/build/create-artifacts.ps1
index 6e305c0..d1d6b92 100644
--- a/build/create-artifacts.ps1
+++ b/build/create-artifacts.ps1
@@ -99,8 +99,8 @@ PluginRelease route53 plugin.validation.dns.route53 @(
"AWSSDK.Route53.dll",
"PKISharp.WACS.Plugins.ValidationPlugins.Route53.dll"
)
-PluginRelease cloudflare plugin.validation.dns.luadns @(
- "PKISharp.WACS.Plugins.ValidationPlugins.LUADNS.dll"
+PluginRelease luadns plugin.validation.dns.luadns @(
+ "PKISharp.WACS.Plugins.ValidationPlugins.LuaDns.dll"
)
PluginRelease cloudflare plugin.validation.dns.cloudflare @(
"FluentCloudflare.dll",
diff --git a/dist/Scripts/ImportADFS.ps1 b/dist/Scripts/ImportADFS.ps1
new file mode 100644
index 0000000..f250f28
--- /dev/null
+++ b/dist/Scripts/ImportADFS.ps1
@@ -0,0 +1,73 @@
+<#
+.SYNOPSIS
+Imports a cert from WACS renewal into Active Directory Federation Services
+.DESCRIPTION
+Note that this script is intended to be run via the install script plugin from win-acme via the batch script wrapper. As such, we use positional parameters to avoid issues with using a dash in the cmd line.
+Note that this script only works on the primary ADFS farm server; you need to make sure to copy the certificates over yourself.
+
+Proper information should be available here
+
+https://github.com/PKISharp/win-acme/wiki/Install-Script
+
+or more generally, here
+
+https://github.com/PKISharp/win-acme/wiki/Example-Scripts
+
+.PARAMETER NewCertThumbprint
+The exact thumbprint of the cert to be imported. The script will copy this cert to the Personal store if not already there.
+
+
+.EXAMPLE
+
+ImportADFS.ps1 <certThumbprint>
+
+.NOTES
+
+#>
+
+param(
+ [Parameter(Position=0,Mandatory=$true)]
+ [string]$NewCertThumbprint
+)
+
+$CertInStore = Get-ChildItem -Path Cert:\LocalMachine -Recurse | Where-Object {$_.thumbprint -eq $NewCertThumbprint} | Sort-Object -Descending | Select-Object -f 1
+if($CertInStore){
+ try{
+ # Cert must exist in the personal store of machine to bind to ADFS
+ if($CertInStore.PSPath -notlike "*LocalMachine\My\*"){
+ $SourceStoreScope = 'LocalMachine'
+ $SourceStorename = $CertInStore.PSParentPath.split("\")[-1]
+
+ $SourceStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $SourceStorename, $SourceStoreScope
+ $SourceStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)
+
+ $cert = $SourceStore.Certificates | Where-Object {$_.thumbprint -eq $CertInStore.Thumbprint}
+
+
+
+ $DestStoreScope = 'LocalMachine'
+ $DestStoreName = 'My'
+
+ $DestStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $DestStoreName, $DestStoreScope
+ $DestStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
+ $DestStore.Add($cert)
+
+
+ $SourceStore.Close()
+ $DestStore.Close()
+
+ $CertInStore = Get-ChildItem -Path Cert:\LocalMachine\My -Recurse | Where-Object {$_.thumbprint -eq $NewCertThumbprint} | Sort-Object -Descending | Select-Object -f 1
+ }
+
+ Set-AdfsCertificate -CertificateType Service-Communications -Thumbprint $CertInStore.Thumbprint -ErrorAction Stop
+ Set-AdfsSslCertificate -Thumbprint $CertInStore.Thumbprint -ErrorAction Stop
+ Restart-Service adfssrv -Force -ErrorAction Stop
+ "Cert thumbprint set to ADFS and service restarted"
+ }catch{
+ "Cert thumbprint was not set successfully"
+ "Error: $($Error[0])"
+ }
+}else{
+ "Cert thumbprint not found in the cert store... which is strange because it should be there."
+}
+
diff --git a/src/ACMESharpCore b/src/ACMESharpCore
-Subproject 697d645d39308a2d2af0259b9616cd729de051b
+Subproject 34712cd284b1506e5472f350ab041738b33ad22
diff --git a/src/main.lib/Clients/Acme/AcmeClient.cs b/src/main.lib/Clients/Acme/AcmeClient.cs
index 2eae016..5d8fed9 100644
--- a/src/main.lib/Clients/Acme/AcmeClient.cs
+++ b/src/main.lib/Clients/Acme/AcmeClient.cs
@@ -1,562 +1,596 @@
-using ACMESharp.Authorizations;
-using ACMESharp.Crypto.JOSE;
-using ACMESharp.Crypto.JOSE.Impl;
-using ACMESharp.Protocol;
-using ACMESharp.Protocol.Resources;
-using Newtonsoft.Json;
-using PKISharp.WACS.Extensions;
-using PKISharp.WACS.Services;
-using PKISharp.WACS.Services.Serialization;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Net.Http;
-using System.Net.Mail;
-using System.Security.Authentication;
-using System.Security.Cryptography;
-using System.Threading.Tasks;
-
-namespace PKISharp.WACS.Clients.Acme
-{
- /// <summary>
- /// Main class that talks to the ACME server
- /// </summary>
- internal class AcmeClient : IDisposable
- {
- private const string RegistrationFileName = "Registration_v2";
- private const string SignerFileName = "Signer_v2";
-
- public const string OrderReady = "ready";
- public const string OrderPending = "pending";
- public const string OrderProcessing = "processing";
- public const string OrderValid = "valid";
-
- public const string AuthorizationValid = "valid";
- public const string AuthorizationInvalid = "invalid";
- public const string AuthorizationPending = "pending";
- public const string AuthorizationProcessing = "processing";
-
- public const string ChallengeValid = "valid";
-
- private readonly ILogService _log;
- private readonly IInputService _input;
- private readonly ISettingsService _settings;
- private readonly IArgumentsService _arguments;
- private readonly ProxyService _proxyService;
-
- private AcmeProtocolClient? _client;
- private AccountSigner? _accountSigner;
- private bool _initialized = false;
-
- public AcmeClient(
- IInputService inputService,
- IArgumentsService arguments,
- ILogService log,
- ISettingsService settings,
- ProxyService proxy)
- {
- _log = log;
- _settings = settings;
- _arguments = arguments;
- _input = inputService;
- _proxyService = proxy;
- }
-
- #region - Account and registration -
-
- internal async Task ConfigureAcmeClient()
- {
- _log.Verbose("Loading ACME account signer...");
- IJwsTool? signer = null;
- var accountSigner = AccountSigner;
- if (accountSigner != null)
- {
- signer = accountSigner.JwsTool();
- }
-
- var httpClient = _proxyService.GetHttpClient();
- httpClient.BaseAddress = _settings.BaseUri;
- var client = PrepareClient(httpClient, signer);
- try
- {
- client.Directory = await client.GetDirectoryAsync();
- }
- catch (Exception)
- {
- // Perhaps the BaseUri *is* the directory, such
- // as implemented by Digicert (#1434)
- client.Directory.Directory = "";
- client.Directory = await client.GetDirectoryAsync();
- }
- await client.GetNonceAsync();
- client.Account = await LoadAccount(client, signer);
- if (client.Account == null)
- {
- throw new Exception("AcmeClient was unable to find or create an account");
- }
- _client = client;
- }
-
- internal AcmeProtocolClient PrepareClient(HttpClient httpClient, IJwsTool? signer)
- {
- AcmeProtocolClient? client = null;
- _log.Verbose("Constructing ACME protocol client...");
- try
- {
- client = new AcmeProtocolClient(
- httpClient,
- signer: signer,
- usePostAsGet: _settings.Acme.PostAsGet);
- }
- catch (CryptographicException)
- {
- if (signer == null)
- {
- // There has been a problem generate a signer for the
- // new account, possibly because some EC curve is not
- // on available on the system? So we give it another
- // shot with a less fancy RSA signer
- _log.Verbose("First chance error generating new signer, retrying with RSA instead of ECC");
- signer = new RSJwsTool
- {
- KeySize = _settings.Security.RSAKeyBits
- };
- signer.Init();
- client = new AcmeProtocolClient(
- httpClient,
- signer: signer,
- usePostAsGet: _settings.Acme.PostAsGet);
- }
- else
- {
- throw;
- }
- }
- client.BeforeHttpSend = (x, r) => _log.Debug("Send {method} request to {uri}", r.Method, r.RequestUri);
- client.AfterHttpSend = (x, r) => _log.Verbose("Request completed with status {s}", r.StatusCode);
- return client;
- }
-
- internal async Task<AccountDetails?> GetAccount() => (await GetClient()).Account;
-
- internal async Task<AcmeProtocolClient> GetClient()
- {
- if (!_initialized)
- {
- await ConfigureAcmeClient();
- _initialized = true;
- }
- if (_client == null)
- {
- throw new InvalidOperationException();
- }
- return _client;
- }
-
- private async Task<AccountDetails?> LoadAccount(AcmeProtocolClient client, IJwsTool? signer)
- {
- AccountDetails? account = null;
- if (File.Exists(AccountPath))
- {
- if (signer != null)
- {
- _log.Debug("Loading account information from {registrationPath}", AccountPath);
- account = JsonConvert.DeserializeObject<AccountDetails>(File.ReadAllText(AccountPath));
- client.Account = account;
- // Maybe we should update the account details
- // on every start of the program to figure out
- // if it hasn't been suspended or cancelled?
- // UpdateAccount();
- }
- else
- {
- _log.Error("Account found but no valid signer could be loaded");
- }
- }
- else
- {
- var contacts = await GetContacts();
- var (_, filename, content) = await client.GetTermsOfServiceAsync();
- if (!string.IsNullOrEmpty(filename))
- {
- if (!await AcceptTos(filename, content))
- {
- return null;
- }
- }
- account = await client.CreateAccountAsync(contacts, termsOfServiceAgreed: true);
- _log.Debug("Saving registration");
- var accountKey = new AccountSigner
- {
- KeyType = client.Signer.JwsAlg,
- KeyExport = client.Signer.Export(),
- };
- AccountSigner = accountKey;
- File.WriteAllText(AccountPath, JsonConvert.SerializeObject(account));
- }
- return account;
- }
-
- /// <summary>
- /// Ask the user to accept the terms of service dictated
- /// by the ACME service operator
- /// </summary>
- /// <param name="filename"></param>
- /// <param name="content"></param>
- /// <returns></returns>
- private async Task<bool> AcceptTos(string filename, byte[] content)
- {
- var tosPath = Path.Combine(_settings.Client.ConfigurationPath, filename);
- File.WriteAllBytes(tosPath, content);
- _input.Show($"Terms of service", tosPath);
- if (_arguments.MainArguments.AcceptTos)
- {
- return true;
- }
- if (await _input.PromptYesNo($"Open in default application?", false))
- {
- try
- {
- Process.Start(new ProcessStartInfo
- {
- FileName = tosPath,
- UseShellExecute = true
- });
- }
- catch (Exception ex)
- {
- _log.Error(ex, "Unable to start application");
- }
- }
- return await _input.PromptYesNo($"Do you agree with the terms?", true);
- }
-
- /// <summary>
- /// Test the network connection
- /// </summary>
- internal async Task CheckNetwork()
- {
- using var httpClient = _proxyService.GetHttpClient();
- httpClient.BaseAddress = _settings.BaseUri;
- try
- {
- _log.Verbose("SecurityProtocol setting: {setting}", System.Net.ServicePointManager.SecurityProtocol);
- _ = await httpClient.GetAsync("directory");
- }
- catch (Exception)
- {
- _log.Warning("No luck yet, attempting to force TLS 1.2...");
- _proxyService.SslProtocols = SslProtocols.Tls12;
- using var altClient = _proxyService.GetHttpClient();
- altClient.BaseAddress = _settings.BaseUri;
- try
- {
- _ = await altClient.GetAsync("directory");
- }
- catch (Exception ex)
- {
- _log.Error(ex, "Unable to connect to ACME server");
- return;
- }
- }
- _log.Debug("Connection OK!");
- }
-
- /// <summary>
- /// Get contact information
- /// </summary>
- /// <returns></returns>
- private async Task<string[]> GetContacts()
- {
- var email = _arguments.MainArguments.EmailAddress;
- if (string.IsNullOrWhiteSpace(email))
- {
- email = await _input.RequestString("Enter email(s) for notifications about problems and abuse (comma seperated)");
- }
- var newEmails = email.ParseCsv();
- if (newEmails == null)
- {
- return new string[] { };
- }
- newEmails = newEmails.Where(x =>
- {
- try
- {
- new MailAddress(x);
- return true;
- }
- catch
- {
- _log.Warning($"Invalid email: {x}");
- return false;
- }
- }).ToList();
- if (!newEmails.Any())
- {
- _log.Warning("No (valid) email addresses specified");
- }
- return newEmails.Select(x => $"mailto:{x}").ToArray();
- }
-
- /// <summary>
- /// File that contains information about the signer, which
- /// cryptographically signs the messages sent to the ACME
- /// server so that the account can be authenticated
- /// </summary>
- private string SignerPath => Path.Combine(_settings.Client.ConfigurationPath, SignerFileName);
-
- /// <summary>
- /// File that contains information about the account
- /// </summary>
- private string AccountPath => Path.Combine(_settings.Client.ConfigurationPath, RegistrationFileName);
-
- private AccountSigner? AccountSigner
- {
- get
- {
- if (_accountSigner == null)
- {
- if (File.Exists(SignerPath))
- {
- try
- {
- _log.Debug("Loading signer from {SignerPath}", SignerPath);
- var signerString = new ProtectedString(File.ReadAllText(SignerPath), _log);
- _accountSigner = JsonConvert.DeserializeObject<AccountSigner>(signerString.Value);
- }
- catch (Exception ex)
- {
- _log.Error(ex, "Unable to load signer");
- }
- }
- }
- return _accountSigner;
- }
- set
- {
- _log.Debug("Saving signer to {SignerPath}", SignerPath);
- var x = new ProtectedString(JsonConvert.SerializeObject(value));
- File.WriteAllText(SignerPath, x.DiskValue(_settings.Security.EncryptConfig));
- _accountSigner = value;
- }
- }
-
- internal void EncryptSigner()
- {
- try
- {
- var signer = AccountSigner;
- AccountSigner = signer; //forces a re-save of the signer
- _log.Information("Signer re-saved");
- }
- catch
- {
- _log.Error("Cannot re-save signer as it is likely encrypted on a different machine");
- }
- }
-
- #endregion
-
- internal async Task<IChallengeValidationDetails> DecodeChallengeValidation(Authorization auth, Challenge challenge)
- {
- var client = await GetClient();
- return AuthorizationDecoder.DecodeChallengeValidation(auth, challenge.Type, client.Signer);
- }
-
- internal async Task<Challenge> AnswerChallenge(Challenge challenge)
- {
- // Have to loop to wait for server to stop being pending
- var client = await GetClient();
- challenge = await Retry(() => client.AnswerChallengeAsync(challenge.Url));
- var tries = 1;
- while (
- challenge.Status == AuthorizationPending ||
- challenge.Status == AuthorizationProcessing)
- {
- await Task.Delay(_settings.Acme.RetryInterval * 1000);
- _log.Debug("Refreshing authorization ({tries}/{count})", tries, _settings.Acme.RetryCount);
- challenge = await Retry(() => client.GetChallengeDetailsAsync(challenge.Url));
- tries += 1;
- if (tries > _settings.Acme.RetryCount)
- {
- break;
- }
- }
- return challenge;
- }
-
- internal async Task<OrderDetails> CreateOrder(IEnumerable<string> identifiers)
- {
- var client = await GetClient();
- return await Retry(() => client.CreateOrderAsync(identifiers));
- }
-
- internal async Task<Challenge> GetChallengeDetails(string url)
- {
- var client = await GetClient();
- return await Retry(() => client.GetChallengeDetailsAsync(url));
- }
-
- internal async Task<Authorization> GetAuthorizationDetails(string url)
- {
- var client = await GetClient();
- return await Retry(() => client.GetAuthorizationDetailsAsync(url));
- }
-
- /// <summary>
- /// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
- /// </summary>
- /// <param name="details"></param>
- /// <param name="csr"></param>
- /// <returns></returns>
- internal async Task<OrderDetails> SubmitCsr(OrderDetails details, byte[] csr)
- {
- // First wait for the order to get "ready", meaning that all validations
- // are complete. The program makes sure this is the case at the level of
- // individual authorizations, but the server might need some extra time to
- // propagate this status at the order level.
- var client = await GetClient();
- await WaitForOrderStatus(details, OrderReady, false);
- if (details.Payload.Status == OrderReady)
- {
- details = await Retry(() => client.FinalizeOrderAsync(details.Payload.Finalize, csr));
- await WaitForOrderStatus(details, OrderProcessing, true);
- }
- return details;
- }
-
- /// <summary>
- /// Helper function to check/refresh order state
- /// </summary>
- /// <param name="details"></param>
- /// <param name="status"></param>
- /// <param name="negate"></param>
- /// <returns></returns>
- private async Task WaitForOrderStatus(OrderDetails details, string status, bool negate)
- {
- // Wait for processing
- _ = await GetClient();
- var tries = 0;
- do
- {
- if (tries > 0)
- {
- if (tries > _settings.Acme.RetryCount)
- {
- break;
- }
- _log.Debug($"Waiting for order to get {(negate ? "NOT " : "")}{{ready}} ({{tries}}/{{count}})", OrderReady, tries, _settings.Acme.RetryCount);
- await Task.Delay(_settings.Acme.RetryInterval * 1000);
- var update = await GetOrderDetails(details.OrderUrl);
- details.Payload = update.Payload;
- }
- tries += 1;
- } while (
- (negate && details.Payload.Status == status) ||
- (!negate && details.Payload.Status != status)
- );
- }
-
- internal async Task<OrderDetails> GetOrderDetails(string url)
- {
- var client = await GetClient();
- return await Retry(() => client.GetOrderDetailsAsync(url));
- }
-
- internal async Task ChangeContacts()
- {
- var client = await GetClient();
- var contacts = await GetContacts();
- var account = await Retry(() => client.UpdateAccountAsync(contacts, client.Account));
- await UpdateAccount();
- }
-
- internal async Task UpdateAccount()
- {
- var client = await GetClient();
- var account = await Retry(() => client.CheckAccountAsync());
- File.WriteAllText(AccountPath, JsonConvert.SerializeObject(account));
- client.Account = account;
- }
-
- internal async Task<byte[]> GetCertificate(OrderDetails order)
- {
- var client = await GetClient();
- return await Retry(() => client.GetOrderCertificateAsync(order));
- }
-
- internal async Task RevokeCertificate(byte[] crt)
- {
- var client = await GetClient();
- _ = await Retry(async () => client.RevokeCertificateAsync(crt, RevokeReason.Unspecified));
- }
-
- /// <summary>
- /// According to the ACME standard, we SHOULD retry calls
- /// if there is an invalid nonce. TODO: check for the proper
- /// exception feedback, now *any* failed request is retried
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="executor"></param>
- /// <returns></returns>
- private async Task<T> Retry<T>(Func<Task<T>> executor, int attempt = 0)
- {
- try
- {
- return await executor();
- }
- catch (AcmeProtocolException apex)
- {
- if (attempt < 3 && apex.ProblemType == ProblemType.BadNonce)
- {
- _log.Warning("First chance error calling into ACME server, retrying with new nonce...");
- var client = await GetClient();
- await client.GetNonceAsync();
- return await Retry(executor, attempt += 1);
- }
- else
- {
- throw;
- }
- }
- }
-
- #region IDisposable Support
- private bool disposedValue = false; // To detect redundant calls
-
- protected virtual void Dispose(bool disposing)
- {
- if (!disposedValue)
- {
- if (disposing && _client != null)
- {
- _client.Dispose();
- }
-
- // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
- // TODO: set large fields to null.
-
- disposedValue = true;
- }
- }
-
- // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
- // ~AcmeClient()
- // {
- // // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
- // Dispose(false);
- // }
-
- // This code added to correctly implement the disposable pattern.
- public void Dispose()
- {
- // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
- Dispose(true);
- // TODO: uncomment the following line if the finalizer is overridden above.
- // GC.SuppressFinalize(this);
- }
- #endregion
- }
+using ACMESharp.Authorizations;
+using ACMESharp.Crypto.JOSE;
+using ACMESharp.Crypto.JOSE.Impl;
+using ACMESharp.Protocol;
+using ACMESharp.Protocol.Resources;
+using Newtonsoft.Json;
+using PKISharp.WACS.Extensions;
+using PKISharp.WACS.Services;
+using PKISharp.WACS.Services.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+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
+{
+ /// <summary>
+ /// Main class that talks to the ACME server
+ /// </summary>
+ internal class AcmeClient : IDisposable
+ {
+ private const string RegistrationFileName = "Registration_v2";
+ private const string SignerFileName = "Signer_v2";
+
+ /// <summary>
+ /// https://tools.ietf.org/html/rfc8555#section-7.1.6
+ /// </summary>
+ public const string OrderPending = "pending"; // new order
+ public const string OrderReady = "ready"; // all authorizations done
+ public const string OrderProcessing = "processing"; // busy issuing
+ public const string OrderInvalid = "invalid"; // validation/order error
+ public const string OrderValid = "valid"; // certificate issued
+
+ public const string AuthorizationValid = "valid";
+ public const string AuthorizationInvalid = "invalid";
+ public const string AuthorizationPending = "pending";
+ public const string AuthorizationProcessing = "processing";
+
+ public const string ChallengeValid = "valid";
+
+ private readonly ILogService _log;
+ private readonly IInputService _input;
+ private readonly ISettingsService _settings;
+ private readonly IArgumentsService _arguments;
+ private readonly ProxyService _proxyService;
+
+ private AcmeProtocolClient? _client;
+ private AccountSigner? _accountSigner;
+ private bool _initialized = false;
+
+ public AcmeClient(
+ IInputService inputService,
+ IArgumentsService arguments,
+ ILogService log,
+ ISettingsService settings,
+ ProxyService proxy)
+ {
+ _log = log;
+ _settings = settings;
+ _arguments = arguments;
+ _input = inputService;
+ _proxyService = proxy;
+ }
+
+ #region - Account and registration -
+
+ internal async Task ConfigureAcmeClient()
+ {
+ _log.Verbose("Loading ACME account signer...");
+ IJwsTool? signer = null;
+ var accountSigner = AccountSigner;
+ if (accountSigner != null)
+ {
+ signer = accountSigner.JwsTool();
+ }
+
+ var httpClient = _proxyService.GetHttpClient();
+ httpClient.BaseAddress = _settings.BaseUri;
+ var client = PrepareClient(httpClient, signer);
+ try
+ {
+ client.Directory = await client.GetDirectoryAsync();
+ }
+ catch (Exception)
+ {
+ // Perhaps the BaseUri *is* the directory, such
+ // as implemented by Digicert (#1434)
+ client.Directory.Directory = "";
+ client.Directory = await client.GetDirectoryAsync();
+ }
+ await client.GetNonceAsync();
+ client.Account = await LoadAccount(client, signer);
+ if (client.Account == null)
+ {
+ throw new Exception("AcmeClient was unable to find or create an account");
+ }
+ _client = client;
+ }
+
+ internal AcmeProtocolClient PrepareClient(HttpClient httpClient, IJwsTool? signer)
+ {
+ _log.Verbose("Constructing ACME protocol client...");
+ AcmeProtocolClient? client;
+ try
+ {
+ client = new AcmeProtocolClient(
+ httpClient,
+ signer: signer,
+ usePostAsGet: _settings.Acme.PostAsGet);
+ }
+ catch (CryptographicException)
+ {
+ if (signer == null)
+ {
+ // There has been a problem generate a signer for the
+ // new account, possibly because some EC curve is not
+ // on available on the system? So we give it another
+ // shot with a less fancy RSA signer
+ _log.Verbose("First chance error generating new signer, retrying with RSA instead of ECC");
+ signer = new RSJwsTool
+ {
+ KeySize = _settings.Security.RSAKeyBits
+ };
+ signer.Init();
+ client = new AcmeProtocolClient(
+ httpClient,
+ signer: signer,
+ usePostAsGet: _settings.Acme.PostAsGet);
+ }
+ else
+ {
+ throw;
+ }
+ }
+ return client;
+ }
+
+ internal async Task<AccountDetails?> GetAccount() => (await GetClient()).Account;
+
+ internal async Task<AcmeProtocolClient> GetClient()
+ {
+ if (!_initialized)
+ {
+ await ConfigureAcmeClient();
+ _initialized = true;
+ }
+ if (_client == null)
+ {
+ throw new InvalidOperationException();
+ }
+ return _client;
+ }
+
+ private async Task<AccountDetails?> LoadAccount(AcmeProtocolClient client, IJwsTool? signer)
+ {
+ AccountDetails? account = null;
+ if (File.Exists(AccountPath))
+ {
+ if (signer != null)
+ {
+ _log.Debug("Loading account information from {registrationPath}", AccountPath);
+ account = JsonConvert.DeserializeObject<AccountDetails>(File.ReadAllText(AccountPath));
+ client.Account = account;
+ // Maybe we should update the account details
+ // on every start of the program to figure out
+ // if it hasn't been suspended or cancelled?
+ // UpdateAccount();
+ }
+ else
+ {
+ _log.Error("Account found but no valid signer could be loaded");
+ }
+ }
+ else
+ {
+ var contacts = await GetContacts();
+ try
+ {
+ var (_, filename, content) = await client.GetTermsOfServiceAsync();
+ if (!string.IsNullOrEmpty(filename))
+ {
+ if (!await AcceptTos(filename, content))
+ {
+ return null;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Error getting terms of service");
+ }
+
+ try
+ {
+ account = await client.CreateAccountAsync(contacts, termsOfServiceAgreed: true);
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Error creating account");
+ }
+
+ try
+ {
+ _log.Debug("Saving account");
+ var accountKey = new AccountSigner
+ {
+ KeyType = client.Signer.JwsAlg,
+ KeyExport = client.Signer.Export(),
+ };
+ AccountSigner = accountKey;
+ File.WriteAllText(AccountPath, JsonConvert.SerializeObject(account));
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Error saving account");
+ account = null;
+ }
+ }
+ return account;
+ }
+
+ /// <summary>
+ /// Ask the user to accept the terms of service dictated
+ /// by the ACME service operator
+ /// </summary>
+ /// <param name="filename"></param>
+ /// <param name="content"></param>
+ /// <returns></returns>
+ private async Task<bool> AcceptTos(string filename, byte[] content)
+ {
+ var tosPath = Path.Combine(_settings.Client.ConfigurationPath, filename);
+ File.WriteAllBytes(tosPath, content);
+ _input.Show($"Terms of service", tosPath);
+ if (_arguments.MainArguments.AcceptTos)
+ {
+ return true;
+ }
+ if (await _input.PromptYesNo($"Open in default application?", false))
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = tosPath,
+ UseShellExecute = true
+ });
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Unable to start application");
+ }
+ }
+ return await _input.PromptYesNo($"Do you agree with the terms?", true);
+ }
+
+ /// <summary>
+ /// Test the network connection
+ /// </summary>
+ internal async Task CheckNetwork()
+ {
+ using var httpClient = _proxyService.GetHttpClient();
+ httpClient.BaseAddress = _settings.BaseUri;
+ try
+ {
+ _log.Verbose("SecurityProtocol setting: {setting}", System.Net.ServicePointManager.SecurityProtocol);
+ _ = await httpClient.GetAsync("directory");
+ }
+ catch (Exception)
+ {
+ _log.Warning("No luck yet, attempting to force TLS 1.2...");
+ _proxyService.SslProtocols = SslProtocols.Tls12;
+ using var altClient = _proxyService.GetHttpClient();
+ altClient.BaseAddress = _settings.BaseUri;
+ try
+ {
+ _ = await altClient.GetAsync("directory");
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Unable to connect to ACME server");
+ return;
+ }
+ }
+ _log.Debug("Connection OK!");
+ }
+
+ /// <summary>
+ /// Get contact information
+ /// </summary>
+ /// <returns></returns>
+ private async Task<string[]> GetContacts()
+ {
+ var email = _arguments.MainArguments.EmailAddress;
+ if (string.IsNullOrWhiteSpace(email))
+ {
+ email = await _input.RequestString("Enter email(s) for notifications about problems and abuse (comma seperated)");
+ }
+ var newEmails = email.ParseCsv();
+ if (newEmails == null)
+ {
+ return new string[] { };
+ }
+ newEmails = newEmails.Where(x =>
+ {
+ try
+ {
+ new MailAddress(x);
+ return true;
+ }
+ catch
+ {
+ _log.Warning($"Invalid email: {x}");
+ return false;
+ }
+ }).ToList();
+ if (!newEmails.Any())
+ {
+ _log.Warning("No (valid) email addresses specified");
+ }
+ return newEmails.Select(x => $"mailto:{x}").ToArray();
+ }
+
+ /// <summary>
+ /// File that contains information about the signer, which
+ /// cryptographically signs the messages sent to the ACME
+ /// server so that the account can be authenticated
+ /// </summary>
+ private string SignerPath => Path.Combine(_settings.Client.ConfigurationPath, SignerFileName);
+
+ /// <summary>
+ /// File that contains information about the account
+ /// </summary>
+ private string AccountPath => Path.Combine(_settings.Client.ConfigurationPath, RegistrationFileName);
+
+ private AccountSigner? AccountSigner
+ {
+ get
+ {
+ if (_accountSigner == null)
+ {
+ if (File.Exists(SignerPath))
+ {
+ try
+ {
+ _log.Debug("Loading signer from {SignerPath}", SignerPath);
+ var signerString = new ProtectedString(File.ReadAllText(SignerPath), _log);
+ _accountSigner = JsonConvert.DeserializeObject<AccountSigner>(signerString.Value);
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Unable to load signer");
+ }
+ }
+ }
+ return _accountSigner;
+ }
+ set
+ {
+ _log.Debug("Saving signer to {SignerPath}", SignerPath);
+ var x = new ProtectedString(JsonConvert.SerializeObject(value));
+ File.WriteAllText(SignerPath, x.DiskValue(_settings.Security.EncryptConfig));
+ _accountSigner = value;
+ }
+ }
+
+ internal void EncryptSigner()
+ {
+ try
+ {
+ var signer = AccountSigner;
+ AccountSigner = signer; //forces a re-save of the signer
+ _log.Information("Signer re-saved");
+ }
+ catch
+ {
+ _log.Error("Cannot re-save signer as it is likely encrypted on a different machine");
+ }
+ }
+
+ #endregion
+
+ internal async Task<IChallengeValidationDetails> DecodeChallengeValidation(Authorization auth, Challenge challenge)
+ {
+ var client = await GetClient();
+ return AuthorizationDecoder.DecodeChallengeValidation(auth, challenge.Type, client.Signer);
+ }
+
+ internal async Task<Challenge> AnswerChallenge(Challenge challenge)
+ {
+ // Have to loop to wait for server to stop being pending
+ var client = await GetClient();
+ challenge = await Retry(() => client.AnswerChallengeAsync(challenge.Url));
+ var tries = 1;
+ while (
+ challenge.Status == AuthorizationPending ||
+ challenge.Status == AuthorizationProcessing)
+ {
+ await Task.Delay(_settings.Acme.RetryInterval * 1000);
+ _log.Debug("Refreshing authorization ({tries}/{count})", tries, _settings.Acme.RetryCount);
+ challenge = await Retry(() => client.GetChallengeDetailsAsync(challenge.Url));
+ tries += 1;
+ if (tries > _settings.Acme.RetryCount)
+ {
+ break;
+ }
+ }
+ return challenge;
+ }
+
+ internal async Task<OrderDetails> CreateOrder(IEnumerable<string> identifiers)
+ {
+ var client = await GetClient();
+ return await Retry(() => client.CreateOrderAsync(identifiers));
+ }
+
+ internal async Task<Challenge> GetChallengeDetails(string url)
+ {
+ var client = await GetClient();
+ return await Retry(() => client.GetChallengeDetailsAsync(url));
+ }
+
+ internal async Task<Authorization> GetAuthorizationDetails(string url)
+ {
+ var client = await GetClient();
+ return await Retry(() => client.GetAuthorizationDetailsAsync(url));
+ }
+
+ /// <summary>
+ /// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
+ /// </summary>
+ /// <param name="details"></param>
+ /// <param name="csr"></param>
+ /// <returns></returns>
+ internal async Task<OrderDetails> SubmitCsr(OrderDetails details, byte[] csr)
+ {
+ // First wait for the order to get "ready", meaning that all validations
+ // are complete. The program makes sure this is the case at the level of
+ // individual authorizations, but the server might need some extra time to
+ // propagate this status at the order level.
+ var client = await GetClient();
+ await WaitForOrderStatus(details, OrderReady);
+ if (details.Payload.Status == OrderReady)
+ {
+ details = await Retry(() => client.FinalizeOrderAsync(details.Payload.Finalize, csr));
+ await WaitForOrderStatus(details, OrderProcessing, true);
+ }
+ return details;
+ }
+
+ /// <summary>
+ /// Helper function to check/refresh order state
+ /// </summary>
+ /// <param name="details"></param>
+ /// <param name="status"></param>
+ /// <param name="negate"></param>
+ /// <returns></returns>
+ private async Task WaitForOrderStatus(OrderDetails details, string status, bool negate = false)
+ {
+ // Wait for processing
+ _ = await GetClient();
+ var tries = 0;
+ do
+ {
+ if (tries > 0)
+ {
+ if (tries > _settings.Acme.RetryCount)
+ {
+ break;
+ }
+ _log.Debug($"Waiting for order to get {(negate ? "NOT " : "")}{{ready}} ({{tries}}/{{count}})", status, tries, _settings.Acme.RetryCount);
+ await Task.Delay(_settings.Acme.RetryInterval * 1000);
+ var update = await GetOrderDetails(details.OrderUrl);
+ details.Payload = update.Payload;
+ }
+ tries += 1;
+ } while (
+ (negate && details.Payload.Status == status) ||
+ (!negate && details.Payload.Status != status)
+ );
+ }
+
+ internal async Task<OrderDetails> GetOrderDetails(string url)
+ {
+ var client = await GetClient();
+ return await Retry(() => client.GetOrderDetailsAsync(url));
+ }
+
+ internal async Task ChangeContacts()
+ {
+ var client = await GetClient();
+ var contacts = await GetContacts();
+ var account = await Retry(() => client.UpdateAccountAsync(contacts, client.Account));
+ await UpdateAccount();
+ }
+
+ internal async Task UpdateAccount()
+ {
+ var client = await GetClient();
+ var account = await Retry(() => client.CheckAccountAsync());
+ File.WriteAllText(AccountPath, JsonConvert.SerializeObject(account));
+ client.Account = account;
+ }
+
+ internal async Task<byte[]> GetCertificate(OrderDetails order)
+ {
+ var client = await GetClient();
+ return await Retry(() => client.GetOrderCertificateAsync(order));
+ }
+
+ internal async Task RevokeCertificate(byte[] crt)
+ {
+ var client = await GetClient();
+ _ = await Retry(async () => client.RevokeCertificateAsync(crt, RevokeReason.Unspecified));
+ }
+
+ /// <summary>
+ /// According to the ACME standard, we SHOULD retry calls
+ /// if there is an invalid nonce. TODO: check for the proper
+ /// exception feedback, now *any* failed request is retried
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="executor"></param>
+ /// <returns></returns>
+ private async Task<T> Retry<T>(Func<Task<T>> executor, int attempt = 0)
+ {
+ await _requestLock.WaitAsync();
+ try
+ {
+ return await executor();
+ }
+ catch (AcmeProtocolException apex)
+ {
+ if (attempt < 3 && apex.ProblemType == ProblemType.BadNonce)
+ {
+ _log.Warning("First chance error calling into ACME server, retrying with new nonce...");
+ var client = await GetClient();
+ await client.GetNonceAsync();
+ return await Retry(executor, attempt += 1);
+ }
+ else
+ {
+ throw;
+ }
+ }
+ finally
+ {
+ _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
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing && _client != null)
+ {
+ _client.Dispose();
+ }
+
+ // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
+ // TODO: set large fields to null.
+
+ disposedValue = true;
+ }
+ }
+
+ // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
+ // ~AcmeClient()
+ // {
+ // // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
+ // Dispose(false);
+ // }
+
+ // This code added to correctly implement the disposable pattern.
+ public void Dispose() =>
+ // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
+ Dispose(true);// TODO: uncomment the following line if the finalizer is overridden above.// GC.SuppressFinalize(this);
+ #endregion
+ }
} \ No newline at end of file
diff --git a/src/main.lib/Clients/Acme/OrderManager.cs b/src/main.lib/Clients/Acme/OrderManager.cs
index b57ac8f..51a30c3 100644
--- a/src/main.lib/Clients/Acme/OrderManager.cs
+++ b/src/main.lib/Clients/Acme/OrderManager.cs
@@ -1,194 +1,192 @@
-using ACMESharp.Protocol;
-using Newtonsoft.Json;
-using PKISharp.WACS.Configuration;
-using PKISharp.WACS.DomainObjects;
-using PKISharp.WACS.Extensions;
-using PKISharp.WACS.Services;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace PKISharp.WACS.Clients.Acme
-{
- /// <summary>
- /// The OrderManager makes sure that we don't hit rate limits
- /// </summary>
- class OrderManager
- {
- private readonly ILogService _log;
- private readonly ISettingsService _settings;
- private readonly AcmeClient _client;
- private readonly DirectoryInfo _orderPath;
- private readonly ICertificateService _certificateService;
- private const string _orderFileExtension = "order.json";
-
- public OrderManager(ILogService log, ISettingsService settings,
- AcmeClient client, ICertificateService certificateService)
- {
- _log = log;
- _client = client;
- _settings = settings;
- _certificateService = certificateService;
- _orderPath = new DirectoryInfo(Path.Combine(settings.Client.ConfigurationPath, "Orders"));
- }
-
- /// <summary>
- /// Get a previously cached order or if its too old, create a new one
- /// </summary>
- /// <param name="renewal"></param>
- /// <param name="target"></param>
- /// <returns></returns>
- public async Task<OrderDetails?> GetOrCreate(Order order, RunLevel runLevel)
- {
- var cacheKey = _certificateService.CacheKey(order);
- var existingOrder = FindRecentOrder(cacheKey);
- if (existingOrder != null)
- {
- try
- {
- if (runLevel.HasFlag(RunLevel.IgnoreCache))
- {
- _log.Warning("Cached order available but not used with the --{switch} switch.",
- nameof(MainArguments.Force).ToLower());
- }
- else
- {
- existingOrder = await RefreshOrder(existingOrder);
- if (existingOrder.Payload.Status == AcmeClient.OrderValid)
- {
- _log.Warning("Using cached order. To force issue of a new certificate within {days} days, " +
- "run with the --{switch} switch. Be ware that you might run into rate limits doing so.",
- _settings.Cache.ReuseDays,
- nameof(MainArguments.Force).ToLower());
- return existingOrder;
- }
- else
- {
- _log.Debug("Cached order has status {status}, discarding", existingOrder.Payload.Status);
- }
- }
- }
- catch (Exception ex)
- {
- _log.Warning("Unable to refresh cached order: {ex}", ex.Message);
- }
- }
- var identifiers = order.Target.GetHosts(false);
- return await CreateOrder(identifiers, cacheKey);
- }
-
- /// <summary>
- /// Update order details from the server
- /// </summary>
- /// <param name="order"></param>
- /// <returns></returns>
- private async Task<OrderDetails> RefreshOrder(OrderDetails order)
- {
- _log.Debug("Refreshing order...");
- var update = await _client.GetOrderDetails(order.OrderUrl);
- order.Payload = update.Payload;
- return order;
- }
-
- private async Task<OrderDetails?> CreateOrder(IEnumerable<string> identifiers, string cacheKey)
- {
- _log.Verbose("Creating order for hosts: {identifiers}", identifiers);
- var order = await _client.CreateOrder(identifiers);
- // Check if the order is valid
- if ((order.Payload.Status != AcmeClient.OrderReady &&
- order.Payload.Status != AcmeClient.OrderPending) ||
- order.Payload.Error != null)
- {
- _log.Error("Failed to create order {url}: {detail}", order.OrderUrl, order.Payload.Error.Detail);
- return null;
- }
- else
- {
- _log.Verbose("Order {url} created", order.OrderUrl);
- SaveOrder(order, cacheKey);
- }
- return order;
- }
-
- /// <summary>
- /// Check if we have a recent order that can be reused
- /// to prevent hitting rate limits
- /// </summary>
- /// <param name="identifiers"></param>
- /// <returns></returns>
- private OrderDetails? FindRecentOrder(string cacheKey)
- {
- DeleteStaleFiles();
- var fi = new FileInfo(Path.Combine(_orderPath.FullName, $"{cacheKey}.{_orderFileExtension}"));
- if (!fi.Exists || !IsValid(fi))
- {
- return null;
- }
- try
- {
- var content = File.ReadAllText(fi.FullName);
- var order = JsonConvert.DeserializeObject<OrderDetails>(content);
- return order;
- }
- catch (Exception ex)
- {
- _log.Warning("Unable to read order cache: {ex}", ex.Message);
- }
- return null;
- }
-
- /// <summary>
- /// Delete files that are not valid anymore
- /// </summary>
- private void DeleteStaleFiles()
- {
- if (_orderPath.Exists)
- {
- var orders = _orderPath.EnumerateFiles($"*.{_orderFileExtension}");
- foreach (var order in orders)
- {
- if (!IsValid(order))
- {
- try
- {
- order.Delete();
- }
- catch (Exception ex)
- {
- _log.Warning("Unable to clean up order cache: {ex}", ex.Message);
- }
- }
- }
- }
- }
-
- /// <summary>
- /// Test if a cached order file is still usable
- /// </summary>
- /// <returns></returns>
- private bool IsValid(FileInfo order) => order.LastWriteTime > DateTime.Now.AddDays(_settings.Cache.ReuseDays * -1);
-
- /// <summary>
- /// Save order to disk
- /// </summary>
- /// <param name="order"></param>
- private void SaveOrder(OrderDetails order, string cacheKey)
- {
- try
- {
- if (!_orderPath.Exists)
- {
- _orderPath.Create();
- }
- var content = JsonConvert.SerializeObject(order);
- var path = Path.Combine(_orderPath.FullName, $"{cacheKey}.{_orderFileExtension}");
- File.WriteAllText(path, content);
- }
- catch (Exception ex)
- {
- _log.Warning("Unable to write to order cache: {ex}", ex.Message);
- }
- }
- }
-}
+using ACMESharp.Protocol;
+using Newtonsoft.Json;
+using PKISharp.WACS.Configuration;
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Extensions;
+using PKISharp.WACS.Services;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace PKISharp.WACS.Clients.Acme
+{
+ /// <summary>
+ /// The OrderManager makes sure that we don't hit rate limits
+ /// </summary>
+ class OrderManager
+ {
+ private readonly ILogService _log;
+ private readonly ISettingsService _settings;
+ private readonly AcmeClient _client;
+ private readonly DirectoryInfo _orderPath;
+ private readonly ICertificateService _certificateService;
+ private const string _orderFileExtension = "order.json";
+
+ public OrderManager(ILogService log, ISettingsService settings,
+ AcmeClient client, ICertificateService certificateService)
+ {
+ _log = log;
+ _client = client;
+ _settings = settings;
+ _certificateService = certificateService;
+ _orderPath = new DirectoryInfo(Path.Combine(settings.Client.ConfigurationPath, "Orders"));
+ }
+
+ /// <summary>
+ /// Get a previously cached order or if its too old, create a new one
+ /// </summary>
+ /// <param name="renewal"></param>
+ /// <param name="target"></param>
+ /// <returns></returns>
+ public async Task<OrderDetails?> GetOrCreate(Order order, RunLevel runLevel)
+ {
+ var cacheKey = _certificateService.CacheKey(order);
+ var existingOrder = default(OrderDetails); // FindRecentOrder(cacheKey);
+ if (existingOrder != null)
+ {
+ try
+ {
+ if (runLevel.HasFlag(RunLevel.IgnoreCache))
+ {
+ _log.Warning("Cached order available but not used with the --{switch} switch.",
+ nameof(MainArguments.Force).ToLower());
+ }
+ else
+ {
+ existingOrder = await RefreshOrder(existingOrder);
+ if (existingOrder.Payload.Status == AcmeClient.OrderValid ||
+ existingOrder.Payload.Status == AcmeClient.OrderReady)
+ {
+ _log.Warning("Using cached order. To force issue of a new certificate within {days} days, " +
+ "run with the --{switch} switch. Be ware that you might run into rate limits doing so.",
+ _settings.Cache.ReuseDays,
+ nameof(MainArguments.Force).ToLower());
+ return existingOrder;
+ }
+ else
+ {
+ _log.Debug("Cached order has status {status}, discarding", existingOrder.Payload.Status);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Warning("Unable to refresh cached order: {ex}", ex.Message);
+ }
+ }
+ var identifiers = order.Target.GetHosts(false);
+ return await CreateOrder(identifiers, cacheKey);
+ }
+
+ /// <summary>
+ /// Update order details from the server
+ /// </summary>
+ /// <param name="order"></param>
+ /// <returns></returns>
+ private async Task<OrderDetails> RefreshOrder(OrderDetails order)
+ {
+ _log.Debug("Refreshing order...");
+ var update = await _client.GetOrderDetails(order.OrderUrl);
+ order.Payload = update.Payload;
+ return order;
+ }
+
+ private async Task<OrderDetails?> CreateOrder(IEnumerable<string> identifiers, string cacheKey)
+ {
+ _log.Verbose("Creating order for hosts: {identifiers}", identifiers);
+ var order = await _client.CreateOrder(identifiers);
+ if (order.Payload.Error != null)
+ {
+ _log.Error("Failed to create order {url}: {detail}", order.OrderUrl, order.Payload.Error.Detail);
+ return null;
+ }
+ else
+ {
+ _log.Verbose("Order {url} created", order.OrderUrl);
+ SaveOrder(order, cacheKey);
+ }
+ return order;
+ }
+
+ /// <summary>
+ /// Check if we have a recent order that can be reused
+ /// to prevent hitting rate limits
+ /// </summary>
+ /// <param name="identifiers"></param>
+ /// <returns></returns>
+ private OrderDetails? FindRecentOrder(string cacheKey)
+ {
+ DeleteStaleFiles();
+ var fi = new FileInfo(Path.Combine(_orderPath.FullName, $"{cacheKey}.{_orderFileExtension}"));
+ if (!fi.Exists || !IsValid(fi))
+ {
+ return null;
+ }
+ try
+ {
+ var content = File.ReadAllText(fi.FullName);
+ var order = JsonConvert.DeserializeObject<OrderDetails>(content);
+ return order;
+ }
+ catch (Exception ex)
+ {
+ _log.Warning("Unable to read order cache: {ex}", ex.Message);
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Delete files that are not valid anymore
+ /// </summary>
+ private void DeleteStaleFiles()
+ {
+ if (_orderPath.Exists)
+ {
+ var orders = _orderPath.EnumerateFiles($"*.{_orderFileExtension}");
+ foreach (var order in orders)
+ {
+ if (!IsValid(order))
+ {
+ try
+ {
+ order.Delete();
+ }
+ catch (Exception ex)
+ {
+ _log.Warning("Unable to clean up order cache: {ex}", ex.Message);
+ }
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Test if a cached order file is still usable
+ /// </summary>
+ /// <returns></returns>
+ private bool IsValid(FileInfo order) => order.LastWriteTime > DateTime.Now.AddDays(_settings.Cache.ReuseDays * -1);
+
+ /// <summary>
+ /// Save order to disk
+ /// </summary>
+ /// <param name="order"></param>
+ private void SaveOrder(OrderDetails order, string cacheKey)
+ {
+ try
+ {
+ if (!_orderPath.Exists)
+ {
+ _orderPath.Create();
+ }
+ var content = JsonConvert.SerializeObject(order);
+ var path = Path.Combine(_orderPath.FullName, $"{cacheKey}.{_orderFileExtension}");
+ File.WriteAllText(path, content);
+ }
+ catch (Exception ex)
+ {
+ _log.Warning("Unable to write to order cache: {ex}", ex.Message);
+ }
+ }
+ }
+}
diff --git a/src/main.lib/Clients/AcmeDnsClient.cs b/src/main.lib/Clients/AcmeDnsClient.cs
index 0159d85..fb6cd0d 100644
--- a/src/main.lib/Clients/AcmeDnsClient.cs
+++ b/src/main.lib/Clients/AcmeDnsClient.cs
@@ -68,14 +68,16 @@ namespace PKISharp.WACS.Clients
if (newReg != null)
{
// Verify correctness
- _input.Show("Domain", domain, true);
+ _input.CreateSpace();
+ _input.Show("Domain", domain);
_input.Show("Record", $"_acme-challenge.{domain}");
_input.Show("Type", "CNAME");
_input.Show("Content", newReg.Fulldomain + ".");
_input.Show("Note", "Some DNS control panels add the final dot automatically. Only one is required.");
if (!await _input.Wait("Please press <Enter> after you've created and verified the record"))
{
- throw new Exception("User aborted");
+ _log.Warning("User aborted");
+ return false;
}
if (await VerifyRegistration(domain, newReg.Fulldomain, interactive))
{
@@ -110,7 +112,7 @@ namespace PKISharp.WACS.Clients
}
else if (interactive && _input != null)
{
- if (!await _input.PromptYesNo("Unable to verify acme-dns configuration, press 'Y' or <ENTER> to retry, or 'N' to skip this step.", true))
+ if (!await _input.PromptYesNo("Unable to verify acme-dns configuration, press 'Y' or <Enter> to retry, or 'N' to skip this step.", true))
{
_log.Warning("Verification of acme-dns configuration skipped.");
return true;
@@ -131,24 +133,20 @@ namespace PKISharp.WACS.Clients
/// <returns></returns>
private async Task<bool> VerifyCname(string domain, string expected, int round)
{
- var dnsClients = await _dnsClient.GetClients(domain, round);
+ var authority = await _dnsClient.GetAuthority(domain, round, false);
+ var result = authority.Nameservers.ToList();
_log.Debug("Configuration will now be checked at name servers: {address}",
- string.Join(", ", dnsClients.Select(x => x.IpAddress)));
+ string.Join(", ", result.Select(x => x.IpAddress)));
// Parallel queries
- var answers = await Task.WhenAll(dnsClients.Select(client => client.LookupClient.QueryAsync($"_acme-challenge.{domain}", DnsClient.QueryType.CNAME)));
+ var answers = await Task.WhenAll(result.Select(client => client.GetCname($"_acme-challenge.{domain}")));
// Loop through results
- for (var i = 0; i < dnsClients.Count(); i++)
+ for (var i = 0; i < result.Count(); i++)
{
- var currentClient = dnsClients[i];
+ var currentClient = result[i];
var currentResult = answers[i];
- var value = currentResult.Answers.CnameRecords().
- Select(cnameRecord => cnameRecord?.CanonicalName?.Value?.TrimEnd('.')).
- Where(txtRecord => txtRecord != null).
- FirstOrDefault();
-
- if (string.Equals(expected, value, StringComparison.CurrentCultureIgnoreCase))
+ if (string.Equals(expected, currentResult, StringComparison.CurrentCultureIgnoreCase))
{
_log.Verbose("Verification of CNAME record successful at server {server}", currentClient.IpAddress);
}
@@ -156,7 +154,7 @@ namespace PKISharp.WACS.Clients
{
_log.Warning("Verification failed, {domain} found value {found} but expected {expected} at server {server}",
$"_acme-challenge.{domain}",
- value ?? "(null)",
+ currentResult ?? "(null)",
expected,
currentClient.IpAddress);
return false;
@@ -202,18 +200,18 @@ namespace PKISharp.WACS.Clients
}
}
- public async Task Update(string domain, string token)
+ public async Task<bool> Update(string domain, string token)
{
var reg = RegistrationForDomain(domain);
if (reg == null)
{
_log.Error("No registration found for domain {domain}", domain);
- return;
+ return false;
}
if (reg.Fulldomain == null)
{
_log.Error("Registration for domain {domain} appears invalid", domain);
- return;
+ return false;
}
if (!await VerifyCname(domain, reg.Fulldomain, 0))
{
@@ -236,10 +234,12 @@ namespace PKISharp.WACS.Clients
JsonConvert.SerializeObject(request),
Encoding.UTF8,
"application/json"));
+ return true;
}
catch (Exception ex)
{
_log.Error(ex, "Error sending update request to acme-dns server at {baseUri} for domain {domain}", _baseUri, domain);
+ return false;
}
}
diff --git a/src/main.lib/Clients/DNS/LookupClientProvider.cs b/src/main.lib/Clients/DNS/LookupClientProvider.cs
index 061eac7..806fff7 100644
--- a/src/main.lib/Clients/DNS/LookupClientProvider.cs
+++ b/src/main.lib/Clients/DNS/LookupClientProvider.cs
@@ -119,28 +119,43 @@ namespace PKISharp.WACS.Clients.DNS
return ret;
}
+ public class DnsLookupResult
+ {
+ public DnsLookupResult(string domain, IEnumerable<LookupClientWrapper> nameServers, DnsLookupResult? cnameFrom = null)
+ {
+ Nameservers = nameServers;
+ Domain = domain;
+ From = cnameFrom;
+ }
+
+ public IEnumerable<LookupClientWrapper> Nameservers { get; set; }
+ public string Domain { get; set; }
+ public DnsLookupResult? From { get; set; }
+ }
+
/// <summary>
/// Get cached list of authoritative name server ip addresses
/// </summary>
/// <param name="domainName"></param>
/// <param name="round"></param>
/// <returns></returns>
- private async Task<IEnumerable<IPAddress>> GetAuthoritativeNameServersForDomain(string domainName, int round)
+ public async Task<DnsLookupResult> GetAuthority(string domainName, int round = 0, bool followCnames = true, DnsLookupResult? from = null)
{
var key = domainName.ToLower().TrimEnd('.');
if (!_authoritativeNs.ContainsKey(key))
{
try
{
- // _acme-challenge.sub.example.co.uk
+ // Example: _acme-challenge.sub.example.co.uk
domainName = domainName.TrimEnd('.');
- // First domain we should try to ask
+ // First domain we should try to ask is the tld (e.g. co.uk)
var rootDomain = _domainParser.GetTLD(domainName);
var testZone = rootDomain;
var client = GetDefaultClient(round);
- // Other sub domains we should try asking:
+ // Other sub domains we should ask:
+ // 1. example
// 1. sub
// 2. _acme-challenge
var remainingParts = domainName.Substring(0, domainName.LastIndexOf(rootDomain))
@@ -158,12 +173,24 @@ namespace PKISharp.WACS.Clients.DNS
_log.Verbose("Querying server {server} about {part}", client.IpAddress, testZone);
using (LogContext.PushProperty("Domain", testZone))
{
- var tempResult = await client.GetAuthoritativeNameServers(testZone, round);
+ var tempResult = await client.GetNameServers(testZone, round);
_authoritativeNs.Add(testZone, tempResult?.ToList() ?? ipSet ?? _defaultNs);
}
}
ipSet = _authoritativeNs[testZone];
- client = Produce(ipSet.OrderBy(x => Guid.NewGuid()).First());
+ client = Produce(ipSet.OrderBy(x => Guid.NewGuid()).First());
+
+ // CNAME only valid for full domain. Subdomains may be
+ // regular records again
+ if (followCnames && testZone == key)
+ {
+ var cname = await client.GetCname(testZone);
+ if (cname != null)
+ {
+ return await GetAuthority(cname, round, true, Produce(key, from));
+ }
+ }
+
if (remainingParts.Any())
{
testZone = $"{remainingParts.First()}.{testZone}";
@@ -187,22 +214,9 @@ namespace PKISharp.WACS.Clients.DNS
_authoritativeNs.Add(key, _defaultNs);
}
}
- return _authoritativeNs[key];
- }
-
- /// <summary>
- /// Caches <see cref="LookupClient"/>s by domainName.
- /// Use <see cref="DefaultClient"/> instead if a name server
- /// for a specific domain name is not required.
- /// </summary>
- /// <param name="domainName"></param>
- /// <returns>Returns an <see cref="ILookupClient"/> using a name
- /// server associated with the specified domain name.</returns>
- public async Task<List<LookupClientWrapper>> GetClients(string domainName, int round = 0)
- {
- var ipSet = await GetAuthoritativeNameServersForDomain(domainName, round);
- return ipSet.Select(ip => Produce(ip)).ToList();
+ return Produce(key, from);
}
+ private DnsLookupResult Produce(string key, DnsLookupResult? parent = null) => new DnsLookupResult(key, _authoritativeNs[key].Select(ip => Produce(ip)), parent);
}
} \ No newline at end of file
diff --git a/src/main.lib/Clients/DNS/LookupClientWrapper.cs b/src/main.lib/Clients/DNS/LookupClientWrapper.cs
index c520f1b..68ac481 100644
--- a/src/main.lib/Clients/DNS/LookupClientWrapper.cs
+++ b/src/main.lib/Clients/DNS/LookupClientWrapper.cs
@@ -2,8 +2,8 @@
using DnsClient.Protocol;
using PKISharp.WACS.Services;
using Serilog.Context;
-using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
@@ -16,7 +16,7 @@ namespace PKISharp.WACS.Clients.DNS
private readonly LookupClientProvider _provider;
private readonly IPAddress? _ipAddress;
- public ILookupClient LookupClient { get; private set; }
+ private ILookupClient _lookupClient { get; set; }
public string IpAddress => _ipAddress?.ToString() ?? "[System]";
public LookupClientWrapper(ILogService logService, IPAddress? ipAddress, LookupClientProvider provider)
@@ -26,16 +26,16 @@ namespace PKISharp.WACS.Clients.DNS
new LookupClientOptions(new[] { _ipAddress }) :
new LookupClientOptions();
clientOptions.UseCache = true;
- LookupClient = new LookupClient(clientOptions);
+ _lookupClient = new LookupClient(clientOptions);
_log = logService;
_provider = provider;
}
- public async Task<IEnumerable<IPAddress>?> GetAuthoritativeNameServers(string domainName, int round)
+ public async Task<IEnumerable<IPAddress>?> GetNameServers(string host, int round)
{
- domainName = domainName.TrimEnd('.');
- _log.Debug("Querying name servers for {part}", domainName);
- var nsResponse = await LookupClient.QueryAsync(domainName, QueryType.NS);
+ host = host.TrimEnd('.');
+ _log.Debug("Querying name servers for {part}", host);
+ var nsResponse = await _lookupClient.QueryAsync(host, QueryType.NS);
var nsRecords = nsResponse.Answers.NsRecords();
var cnameRecords = nsResponse.Answers.CnameRecords();
if (!nsRecords.Any() && !cnameRecords.Any())
@@ -44,19 +44,24 @@ namespace PKISharp.WACS.Clients.DNS
}
if (nsRecords.Any())
{
- return GetNameServerIpAddresses(nsRecords.Select(n => n.NSDName.Value), round);
+ return GetIpAddresses(nsRecords.Select(n => n.NSDName.Value), round);
}
+ //if (cnameRecords.Any())
+ //{
+ // var client = await _provider.GetClients(cnameRecords.First().CanonicalName.Value);
+ // return await _provider.GetAuthoritativeNameServersForDomain(cnameRecords.First().CanonicalName.Value, round);
+ //}
return null;
}
- private IEnumerable<IPAddress> GetNameServerIpAddresses(IEnumerable<string> nsRecords, int round)
+ private IEnumerable<IPAddress> GetIpAddresses(IEnumerable<string> hosts, int round)
{
- foreach (var nsRecord in nsRecords)
+ foreach (var nsRecord in hosts)
{
using (LogContext.PushProperty("NameServer", nsRecord))
{
_log.Verbose("Querying IP for name server");
- var aResponse = _provider.GetDefaultClient(round).LookupClient.Query(nsRecord, QueryType.A);
+ var aResponse = _provider.GetDefaultClient(round)._lookupClient.Query(nsRecord, QueryType.A);
var nameServerIp = aResponse.Answers.ARecords().FirstOrDefault()?.Address;
if (nameServerIp != null)
{
@@ -67,35 +72,29 @@ namespace PKISharp.WACS.Clients.DNS
}
}
- public async Task<(IEnumerable<string>, string)> GetTextRecordValues(string challengeUri, int attempt)
+ public async Task<IEnumerable<string>> GetTxtRecords(string host)
{
- var result = await LookupClient.QueryAsync(challengeUri, QueryType.TXT);
- string server;
- (result, server) = await RecursivelyFollowCnames(challengeUri, result, attempt);
-
- var txtRecords = result.Answers.TxtRecords().
+ var txtResult = await _lookupClient.QueryAsync(host, QueryType.TXT);
+ return txtResult.Answers.TxtRecords().
SelectMany(txtRecord => txtRecord?.EscapedText).
Where(txtRecord => txtRecord != null).
OfType<string>().
ToList();
-
- return (txtRecords, server);
}
-
- private async Task<(IDnsQueryResponse, string)> RecursivelyFollowCnames(string server, IDnsQueryResponse result, int attempt)
+
+ public async Task<string?> GetCname(string host)
{
- if (result.Answers.CnameRecords().Any())
+ var cnames = await _lookupClient.QueryAsync(host, QueryType.CNAME);
+ var cnameRecords = cnames.Answers.CnameRecords();
+ if (cnameRecords.Any())
{
- server = result.Answers.CnameRecords().First().CanonicalName;
- var recursiveClients = await _provider.GetClients(server, attempt);
- var index = attempt % recursiveClients.Count();
- var recursiveClient = recursiveClients.ElementAt(index);
- var txtResponse = await recursiveClient.LookupClient.QueryAsync(server, QueryType.TXT);
- _log.Debug("Name server {NameServerIpAddress} selected", txtResponse.NameServer.Address.ToString());
- return await recursiveClient.RecursivelyFollowCnames(server, txtResponse, attempt);
+ var canonicalName = cnameRecords.First().CanonicalName.Value;
+ var idn = new IdnMapping();
+ canonicalName = canonicalName.ToLower().Trim().TrimEnd('.');
+ canonicalName = idn.GetAscii(canonicalName);
+ return canonicalName;
}
- return (result, server);
+ return null;
}
-
}
} \ No newline at end of file
diff --git a/src/main.lib/Clients/EmailClient.cs b/src/main.lib/Clients/EmailClient.cs
index a1a111d..4f77115 100644
--- a/src/main.lib/Clients/EmailClient.cs
+++ b/src/main.lib/Clients/EmailClient.cs
@@ -43,7 +43,10 @@ namespace PKISharp.WACS.Clients
_secure = _settings.Notification.SmtpSecure;
_secureMode = _settings.Notification.SmtpSecureMode;
_senderName = _settings.Notification.SenderName;
- _computerName = _settings.Notification.ComputerName ?? Environment.MachineName;
+ _computerName = _settings.Notification.ComputerName;
+ if (string.IsNullOrEmpty(_computerName)) {
+ _computerName = Environment.MachineName;
+ }
_version = Assembly.GetEntryAssembly().GetName().Version.ToString();
if (string.IsNullOrWhiteSpace(_senderName))
@@ -63,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)
{
@@ -96,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)
{
@@ -112,23 +115,28 @@ namespace PKISharp.WACS.Clients
Priority = priority,
Subject = subject
};
- message.Subject = subject;
+ message.Subject = $"{subject} ({_computerName})";
message.From.Add(sender);
message.To.Add(receiver);
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)
{
@@ -137,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/BindingOptions.cs b/src/main.lib/Clients/IIS/BindingOptions.cs
index f9555b5..a69ab9b 100644
--- a/src/main.lib/Clients/IIS/BindingOptions.cs
+++ b/src/main.lib/Clients/IIS/BindingOptions.cs
@@ -1,4 +1,6 @@
using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
namespace PKISharp.WACS.Clients.IIS
{
@@ -47,7 +49,28 @@ namespace PKISharp.WACS.Clients.IIS
/// <summary>
/// Binding string to use in IIS
/// </summary>
- public string Binding => $"{IP}:{Port}:{Host}";
+ public string Binding
+ {
+ get
+ {
+ var formattedIP = IP;
+ if (!string.IsNullOrEmpty(formattedIP))
+ {
+ if (formattedIP != "*")
+ {
+ if (IPAddress.TryParse(formattedIP, out var address))
+ {
+ if (address.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ formattedIP = $"[{formattedIP}]";
+ }
+ }
+ }
+ }
+ return $"{formattedIP}:{Port}:{Host}";
+ }
+ }
+
public override string ToString() => Binding;
/// <summary>
diff --git a/src/main.lib/Clients/IIS/IISClient.cs b/src/main.lib/Clients/IIS/IISClient.cs
index a0b82ee..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;
}
@@ -201,10 +208,10 @@ namespace PKISharp.WACS.Clients.IIS
public void AddBinding(IISSiteWrapper site, BindingOptions options)
{
var newBinding = site.Site.Bindings.CreateElement("binding");
- newBinding.Protocol = "https";
newBinding.BindingInformation = options.Binding;
newBinding.CertificateStoreName = options.Store;
newBinding.CertificateHash = options.Thumbprint;
+ newBinding.Protocol = "https";
if (options.Flags > 0)
{
newBinding.SetAttributeValue("sslFlags", options.Flags);
@@ -223,10 +230,10 @@ namespace PKISharp.WACS.Clients.IIS
"certificateHash"
};
var replacement = site.Site.Bindings.CreateElement("binding");
- replacement.Protocol = existingBinding.Protocol;
replacement.BindingInformation = existingBinding.BindingInformation;
replacement.CertificateStoreName = options.Store;
replacement.CertificateHash = options.Thumbprint;
+ replacement.Protocol = existingBinding.Protocol;
foreach (var attr in existingBinding.Binding.Attributes)
{
try
diff --git a/src/main.lib/Clients/ScriptClient.cs b/src/main.lib/Clients/ScriptClient.cs
index 0d0e4f6..a4e7723 100644
--- a/src/main.lib/Clients/ScriptClient.cs
+++ b/src/main.lib/Clients/ScriptClient.cs
@@ -80,7 +80,7 @@ namespace PKISharp.WACS.Clients
process.EnableRaisingEvents = true;
process.Exited += (s, e) =>
{
- _log.Information(LogType.Event | LogType.Disk, output.ToString());
+ _log.Information(LogType.Event | LogType.Disk | LogType.Notification, output.ToString());
exited = true;
if (process.ExitCode != 0)
{
diff --git a/src/main.lib/Configuration/BaseArgumentsProvider.cs b/src/main.lib/Configuration/BaseArgumentsProvider.cs
index e365084..df42949 100644
--- a/src/main.lib/Configuration/BaseArgumentsProvider.cs
+++ b/src/main.lib/Configuration/BaseArgumentsProvider.cs
@@ -8,6 +8,7 @@ namespace PKISharp.WACS.Configuration
public abstract class BaseArgumentsProvider<T> : IArgumentsProvider<T> where T : class, new()
{
private readonly FluentCommandLineParser<T> _parser;
+ public ILogService? Log { get; set; }
public BaseArgumentsProvider()
{
@@ -67,31 +68,36 @@ namespace PKISharp.WACS.Configuration
return false;
}
- public virtual bool Validate(ILogService log, T current, MainArguments main)
+ public virtual bool Validate(T current, MainArguments main)
{
if (main.Renew)
{
if (IsActive(current))
{
- log.Error($"Renewal {(string.IsNullOrEmpty(Group)?"":$"{Group} ")}parameters cannot be changed during a renewal. Recreate/overwrite the renewal or edit the .json file if you want to make changes.");
+ Log?.Error($"Renewal {(string.IsNullOrEmpty(Group)?"":$"{Group} ")}parameters cannot be changed during a renewal. Recreate/overwrite the renewal or edit the .json file if you want to make changes.");
return false;
}
}
return true;
}
- bool IArgumentsProvider.Validate(ILogService log, object current, MainArguments main) => Validate(log, (T)current, main);
+ bool IArgumentsProvider.Validate(object current, MainArguments main) => Validate((T)current, main);
public IEnumerable<ICommandLineOption> Configuration => _parser.Options;
public ICommandLineParserResult GetParseResult(string[] args) => _parser.Parse(args);
- public T GetResult(string[] args)
+ public T? GetResult(string[] args)
{
- _parser.Parse(args);
+ var result = _parser.Parse(args);
+ if (result.HasErrors)
+ {
+ Log?.Error(result.ErrorText);
+ return null;
+ }
return _parser.Object;
}
- object IArgumentsProvider.GetResult(string[] args) => GetResult(args);
+ object? IArgumentsProvider.GetResult(string[] args) => GetResult(args);
}
} \ No newline at end of file
diff --git a/src/main.lib/Configuration/NetworkCredentialOptions.cs b/src/main.lib/Configuration/NetworkCredentialOptions.cs
index 51054b7..8417aa9 100644
--- a/src/main.lib/Configuration/NetworkCredentialOptions.cs
+++ b/src/main.lib/Configuration/NetworkCredentialOptions.cs
@@ -31,15 +31,15 @@ namespace PKISharp.WACS.Configuration
public NetworkCredentialOptions(IArgumentsService arguments)
{
var args = arguments.GetArguments<NetworkCredentialArguments>();
- UserName = arguments.TryGetRequiredArgument(nameof(args.UserName), args.UserName);
- Password = new ProtectedString(arguments.TryGetRequiredArgument(nameof(args.Password), args.Password));
+ UserName = arguments.TryGetRequiredArgument(nameof(args.UserName), args?.UserName);
+ Password = new ProtectedString(arguments.TryGetRequiredArgument(nameof(args.Password), args?.Password));
}
public NetworkCredentialOptions(IArgumentsService arguments, IInputService input)
{
var args = arguments.GetArguments<NetworkCredentialArguments>();
- UserName = arguments.TryGetArgument(args.UserName, input, "Username").Result;
- Password = new ProtectedString(arguments.TryGetArgument(args.Password, input, "Password", true).Result);
+ UserName = arguments.TryGetArgument(args?.UserName, input, "Username").Result;
+ Password = new ProtectedString(arguments.TryGetArgument(args?.Password, input, "Password", true).Result);
}
}
}
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/CsrPlugins/Ec/EcOptionsFactory.cs b/src/main.lib/Plugins/CsrPlugins/Ec/EcOptionsFactory.cs
index 3898e8f..2597cc8 100644
--- a/src/main.lib/Plugins/CsrPlugins/Ec/EcOptionsFactory.cs
+++ b/src/main.lib/Plugins/CsrPlugins/Ec/EcOptionsFactory.cs
@@ -17,8 +17,8 @@ namespace PKISharp.WACS.Plugins.CsrPlugins
var args = _arguments.GetArguments<CsrArguments>();
return Task.FromResult(new EcOptions()
{
- OcspMustStaple = args.OcspMustStaple ? true : (bool?)null,
- ReusePrivateKey = args.ReusePrivateKey ? true : (bool?)null
+ OcspMustStaple = args?.OcspMustStaple ?? null,
+ ReusePrivateKey = args?.ReusePrivateKey ?? null
});
}
}
diff --git a/src/main.lib/Plugins/CsrPlugins/Rsa/RsaOptionsFactory.cs b/src/main.lib/Plugins/CsrPlugins/Rsa/RsaOptionsFactory.cs
index 1c34bf3..5ae93f0 100644
--- a/src/main.lib/Plugins/CsrPlugins/Rsa/RsaOptionsFactory.cs
+++ b/src/main.lib/Plugins/CsrPlugins/Rsa/RsaOptionsFactory.cs
@@ -20,8 +20,8 @@ namespace PKISharp.WACS.Plugins.CsrPlugins
var args = _arguments.GetArguments<CsrArguments>();
return Task.FromResult(new RsaOptions()
{
- OcspMustStaple = args.OcspMustStaple ? true : (bool?)null,
- ReusePrivateKey = args.ReusePrivateKey ? true : (bool?)null
+ OcspMustStaple = args?.OcspMustStaple ?? null,
+ ReusePrivateKey = args?.ReusePrivateKey ?? null
});
}
}
diff --git a/src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtpOptionsFactory.cs b/src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtpOptionsFactory.cs
index 92b78c5..f685d14 100644
--- a/src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtpOptionsFactory.cs
+++ b/src/main.lib/Plugins/InstallationPlugins/IISFtp/IISFtpOptionsFactory.cs
@@ -40,7 +40,7 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins
{
var args = _arguments.GetArguments<IISFtpArguments>();
var ret = new IISFtpOptions();
- var siteId = args.FtpSiteId;
+ var siteId = args?.FtpSiteId;
if (siteId == null)
{
throw new Exception($"Missing parameter --{nameof(args.FtpSiteId).ToLower()}");
diff --git a/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebArgumentsProvider.cs b/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebArgumentsProvider.cs
index f690e69..8b00da3 100644
--- a/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebArgumentsProvider.cs
+++ b/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebArgumentsProvider.cs
@@ -15,9 +15,9 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins
public override string Group => "Installation";
public override string Condition => "--installation iis";
- public override bool Validate(ILogService log, IISWebArguments current, MainArguments main)
+ public override bool Validate(IISWebArguments current, MainArguments main)
{
- if (!base.Validate(log, current, main))
+ if (!base.Validate(current, main))
{
return false;
}
@@ -27,13 +27,13 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins
{
if (port < 1 || port > 65535)
{
- log.Error("Invalid --{param}, value should be between 1 and 65535", SslPortParameterName);
+ Log?.Error("Invalid --{param}, value should be between 1 and 65535", SslPortParameterName);
return false;
}
}
else
{
- log.Error("Invalid --{param}, value should be a number", SslPortParameterName);
+ Log?.Error("Invalid --{param}, value should be a number", SslPortParameterName);
return false;
}
}
@@ -41,7 +41,7 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins
{
if (!IPAddress.TryParse(current.SSLIPAddress, out _))
{
- log.Error("Invalid --{sslipaddress}", SslIpParameterName);
+ Log?.Error("Invalid --{sslipaddress}", SslIpParameterName);
return false;
}
}
diff --git a/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebOptions.cs b/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebOptions.cs
index a3a6cf8..490c8c7 100644
--- a/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebOptions.cs
+++ b/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebOptions.cs
@@ -15,14 +15,14 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins
public override string Description => "Create or update https bindings in IIS";
public IISWebOptions() { }
- public IISWebOptions(IISWebArguments args)
+ public IISWebOptions(IISWebArguments? args)
{
- var sslIp = args.SSLIPAddress;
+ var sslIp = args?.SSLIPAddress;
if (!string.IsNullOrEmpty(sslIp) && sslIp != IISClient.DefaultBindingIp)
{
NewBindingIp = sslIp;
}
- var sslPortRaw = args.SSLPort;
+ var sslPortRaw = args?.SSLPort;
if (!string.IsNullOrEmpty(sslPortRaw))
{
// Already validated by the ArgumentsProvider
diff --git a/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebOptionsFactory.cs b/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebOptionsFactory.cs
index 4bb9431..7cdd2a7 100644
--- a/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebOptionsFactory.cs
+++ b/src/main.lib/Plugins/InstallationPlugins/IISWeb/IISWebOptionsFactory.cs
@@ -50,7 +50,7 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins
{
var args = _arguments.GetArguments<IISWebArguments>();
var ret = new IISWebOptions(args);
- if (args.InstallationSiteId != null)
+ if (args?.InstallationSiteId != null)
{
// Throws exception when not found
var site = _iisClient.GetWebSite(args.InstallationSiteId.Value);
diff --git a/src/main.lib/Plugins/InstallationPlugins/Script/ScriptOptionsFactory.cs b/src/main.lib/Plugins/InstallationPlugins/Script/ScriptOptionsFactory.cs
index 8037020..4cb2430 100644
--- a/src/main.lib/Plugins/InstallationPlugins/Script/ScriptOptionsFactory.cs
+++ b/src/main.lib/Plugins/InstallationPlugins/Script/ScriptOptionsFactory.cs
@@ -27,7 +27,7 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins
inputService.Show("Full instructions", "https://www.win-acme.com/reference/plugins/installation/script");
do
{
- ret.Script = await _arguments.TryGetArgument(args.Script, inputService, "Enter the path to the script that you want to run after renewal");
+ ret.Script = await _arguments.TryGetArgument(args?.Script, inputService, "Enter the path to the script that you want to run after renewal");
}
while (!ret.Script.ValidFile(_log));
@@ -39,8 +39,13 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins
inputService.Show("{StoreType}", $"Type of store ({CentralSslOptions.PluginName}/{CertificateStoreOptions.PluginName}/{PemFilesOptions.PluginName})");
inputService.Show("{StorePath}", "Path to the store");
inputService.Show("{RenewalId}", "Renewal identifier");
-
- ret.ScriptParameters = await _arguments.TryGetArgument(args.ScriptParameters, inputService, "Enter the parameter format string for the script, e.g. \"--hostname {CertCommonName}\"");
+ inputService.Show("{OldCertCommonName}", "Common name (primary domain name) of the previously issued certificate");
+ inputService.Show("{OldCertFriendlyName}", "Friendly name of the previously issued certificate");
+ inputService.Show("{OldCertThumbprint}", "Thumbprint of the previously issued certificate");
+ ret.ScriptParameters = await _arguments.TryGetArgument(
+ args?.ScriptParameters,
+ inputService,
+ "Enter the parameter format string for the script, e.g. \"--hostname {CertCommonName}\"");
return ret;
}
@@ -49,13 +54,13 @@ namespace PKISharp.WACS.Plugins.InstallationPlugins
var args = _arguments.GetArguments<ScriptArguments>();
var ret = new ScriptOptions
{
- Script = _arguments.TryGetRequiredArgument(nameof(args.Script), args.Script)
+ Script = _arguments.TryGetRequiredArgument(nameof(args.Script), args?.Script)
};
if (!ret.Script.ValidFile(_log))
{
throw new ArgumentException(nameof(args.Script));
}
- ret.ScriptParameters = args.ScriptParameters;
+ ret.ScriptParameters = args?.ScriptParameters;
return Task.FromResult(ret);
}
}
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/OrderPlugins/Domain/Domain.cs b/src/main.lib/Plugins/OrderPlugins/Domain/Domain.cs
new file mode 100644
index 0000000..89266f1
--- /dev/null
+++ b/src/main.lib/Plugins/OrderPlugins/Domain/Domain.cs
@@ -0,0 +1,64 @@
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Extensions;
+using PKISharp.WACS.Plugins.Interfaces;
+using PKISharp.WACS.Services;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PKISharp.WACS.Plugins.OrderPlugins
+{
+ class Domain : IOrderPlugin
+ {
+ private readonly DomainParseService _domainParseService;
+
+ public Domain(DomainParseService domainParseService) => _domainParseService = domainParseService;
+
+ public IEnumerable<Order> Split(Renewal renewal, Target target)
+ {
+ var ret = new Dictionary<string, Order>();
+ var parts = new Dictionary<string, List<TargetPart>>();
+ foreach (var part in target.Parts)
+ {
+ foreach (var host in part.GetHosts(true))
+ {
+ var domain = _domainParseService.GetRegisterableDomain(host.TrimStart('.', '*'));
+ var sourceParts = target.Parts.Where(p => p.GetHosts(true).Contains(host));
+ if (!ret.ContainsKey(domain))
+ {
+ var filteredParts = sourceParts.Select(p => new TargetPart(new List<string> { host }) { SiteId = p.SiteId }).ToList();
+ var newTarget = new Target(
+ target.FriendlyName ?? "",
+ target.CommonName,
+ filteredParts);
+ var newOrder = new Order(
+ renewal,
+ newTarget,
+ friendlyNamePart: domain,
+ cacheKeyPart: domain);
+ ret.Add(domain, newOrder);
+ parts.Add(domain, filteredParts);
+ }
+ else
+ {
+ var existingOrder = ret[domain];
+ var existingParts = parts[domain];
+ foreach (var x in sourceParts)
+ {
+ var existingPart = existingParts.Where(x => x.SiteId == x.SiteId).FirstOrDefault();
+ if (existingPart == null)
+ {
+ existingPart = new TargetPart(new[] { host });
+ existingParts.Add(existingPart);
+ }
+ else if (!existingPart.Identifiers.Contains(host))
+ {
+ existingPart.Identifiers.Add(host);
+ }
+ }
+ }
+ }
+ }
+ return ret.Values;
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/OrderPlugins/Domain/DomainOptions.cs b/src/main.lib/Plugins/OrderPlugins/Domain/DomainOptions.cs
new file mode 100644
index 0000000..fdee3da
--- /dev/null
+++ b/src/main.lib/Plugins/OrderPlugins/Domain/DomainOptions.cs
@@ -0,0 +1,12 @@
+using PKISharp.WACS.Plugins.Base;
+using PKISharp.WACS.Plugins.Base.Options;
+
+namespace PKISharp.WACS.Plugins.OrderPlugins
+{
+ [Plugin("b7c331d4-d875-453e-b83a-2b537ca12535")]
+ internal class DomainOptions : OrderPluginOptions<Domain>
+ {
+ public override string Name => "Domain";
+ public override string Description => "Seperate certificate for each domain";
+ }
+}
diff --git a/src/main.lib/Plugins/OrderPlugins/Domain/DomainOptionsFactory.cs b/src/main.lib/Plugins/OrderPlugins/Domain/DomainOptionsFactory.cs
new file mode 100644
index 0000000..203487b
--- /dev/null
+++ b/src/main.lib/Plugins/OrderPlugins/Domain/DomainOptionsFactory.cs
@@ -0,0 +1,14 @@
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Plugins.Base.Factories;
+using PKISharp.WACS.Services;
+using System.Threading.Tasks;
+
+namespace PKISharp.WACS.Plugins.OrderPlugins
+{
+ class DomainOptionsFactory : OrderPluginOptionsFactory<Domain, DomainOptions>
+ {
+ public override bool CanProcess(Target target) => target.CsrBytes == null;
+ public override Task<DomainOptions> Aquire(IInputService inputService, RunLevel runLevel) => Default();
+ public override Task<DomainOptions> Default() => Task.FromResult(new DomainOptions());
+ }
+}
diff --git a/src/main.lib/Plugins/OrderPlugins/Host/Host.cs b/src/main.lib/Plugins/OrderPlugins/Host/Host.cs
index 7cc3f29..2488b74 100644
--- a/src/main.lib/Plugins/OrderPlugins/Host/Host.cs
+++ b/src/main.lib/Plugins/OrderPlugins/Host/Host.cs
@@ -27,7 +27,7 @@ namespace PKISharp.WACS.Plugins.OrderPlugins
renewal,
newTarget,
friendlyNamePart: host,
- cacheKeyPart: $"{host}|{part.SiteId ?? -1}");
+ cacheKeyPart: host);
ret.Add(newOrder);
seen.Add(host);
}
diff --git a/src/main.lib/Plugins/OrderPlugins/Host/HostOptionsFactory.cs b/src/main.lib/Plugins/OrderPlugins/Host/HostOptionsFactory.cs
index 719f1eb..ddd53c4 100644
--- a/src/main.lib/Plugins/OrderPlugins/Host/HostOptionsFactory.cs
+++ b/src/main.lib/Plugins/OrderPlugins/Host/HostOptionsFactory.cs
@@ -7,7 +7,7 @@ namespace PKISharp.WACS.Plugins.OrderPlugins
{
class HostOptionsFactory : OrderPluginOptionsFactory<Host, HostOptions>
{
- public override bool CanProcess(Target target) => true;
+ public override bool CanProcess(Target target) => target.CsrBytes == null;
public override Task<HostOptions> Aquire(IInputService inputService, RunLevel runLevel) => Default();
public override Task<HostOptions> Default() => Task.FromResult(new HostOptions());
}
diff --git a/src/main.lib/Plugins/OrderPlugins/Site/Site.cs b/src/main.lib/Plugins/OrderPlugins/Site/Site.cs
new file mode 100644
index 0000000..7a300b5
--- /dev/null
+++ b/src/main.lib/Plugins/OrderPlugins/Site/Site.cs
@@ -0,0 +1,28 @@
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Plugins.Interfaces;
+using System.Collections.Generic;
+
+namespace PKISharp.WACS.Plugins.OrderPlugins
+{
+ class Site : IOrderPlugin
+ {
+ public IEnumerable<Order> Split(Renewal renewal, Target target)
+ {
+ var ret = new List<Order>();
+ foreach (var part in target.Parts)
+ {
+ var newTarget = new Target(
+ target.FriendlyName ?? "",
+ target.CommonName,
+ new List<TargetPart> { part });
+ var newOrder = new Order(
+ renewal,
+ newTarget,
+ friendlyNamePart: $"site {part.SiteId ?? -1}",
+ cacheKeyPart: $"{part.SiteId ?? -1}");
+ ret.Add(newOrder);
+ }
+ return ret;
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/OrderPlugins/Site/SiteOptions.cs b/src/main.lib/Plugins/OrderPlugins/Site/SiteOptions.cs
new file mode 100644
index 0000000..ffe15f6
--- /dev/null
+++ b/src/main.lib/Plugins/OrderPlugins/Site/SiteOptions.cs
@@ -0,0 +1,12 @@
+using PKISharp.WACS.Plugins.Base;
+using PKISharp.WACS.Plugins.Base.Options;
+
+namespace PKISharp.WACS.Plugins.OrderPlugins
+{
+ [Plugin("74a42b2d-8eaa-4f40-ab6a-f55304254143")]
+ internal class SiteOptions : OrderPluginOptions<Site>
+ {
+ public override string Name => "Site";
+ public override string Description => "Seperate certificate for each site";
+ }
+}
diff --git a/src/main.lib/Plugins/OrderPlugins/Site/SiteOptionsFactory.cs b/src/main.lib/Plugins/OrderPlugins/Site/SiteOptionsFactory.cs
new file mode 100644
index 0000000..42ed45d
--- /dev/null
+++ b/src/main.lib/Plugins/OrderPlugins/Site/SiteOptionsFactory.cs
@@ -0,0 +1,14 @@
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Plugins.Base.Factories;
+using PKISharp.WACS.Services;
+using System.Threading.Tasks;
+
+namespace PKISharp.WACS.Plugins.OrderPlugins
+{
+ class SiteOptionsFactory : OrderPluginOptionsFactory<Site, SiteOptions>
+ {
+ public override bool CanProcess(Target target) => target.CsrBytes == null;
+ public override Task<SiteOptions> Aquire(IInputService inputService, RunLevel runLevel) => Default();
+ public override Task<SiteOptions> Default() => Task.FromResult(new SiteOptions());
+ }
+}
diff --git a/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs b/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs
index 2c682ec..dc96981 100644
--- a/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs
+++ b/src/main.lib/Plugins/Resolvers/InteractiveResolver.cs
@@ -129,7 +129,8 @@ namespace PKISharp.WACS.Plugins.Resolvers
// List options for generating new certificates
if (!string.IsNullOrEmpty(longDescription))
{
- _input.Show(null, longDescription, true);
+ _input.CreateSpace();
+ _input.Show(null, longDescription);
}
Choice<IPluginOptionsFactory?> creator(T plugin, Type type, (bool, string?) disabled) {
@@ -184,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 =>
@@ -202,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(),
@@ -249,13 +260,17 @@ 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()] :
"";
return await GetPlugin<IStorePluginOptionsFactory>(
scope,
- filter: (x) => x.Except(chosen),
+ filter: (x) => x, // Disable default null check
defaultParam1: defaultParam1,
defaultType: defaultType,
defaultTypeFallback: typeof(PemFilesOptionsFactory),
@@ -291,13 +306,17 @@ 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()] :
"";
return await GetPlugin<IInstallationPluginOptionsFactory>(
scope,
- filter: (x) => x.Except(chosen),
+ filter: (x) => x, // Disable default null check
unusable: x => (!x.CanInstall(storeTypes), "This step cannot be used in combination with the specified store(s)"),
defaultParam1: defaultParam1,
defaultType: defaultType,
diff --git a/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs b/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs
index a4d0d09..e8fd230 100644
--- a/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs
+++ b/src/main.lib/Plugins/Resolvers/UnattendedResolver.cs
@@ -98,7 +98,7 @@ namespace PKISharp.WACS.Plugins.Resolvers
{
_log.Error("{n} plugin {x} not available: {m}. " + changeInstructions,
char.ToUpper(className[0]) + className.Substring(1),
- (defaultOption.plugin as IPluginOptionsFactory)?.Name ?? "Unknown",
+ defaultOption.plugin?.Name ?? "Unknown",
defaultTypeDisabled.Item2);
return nullResult;
}
diff --git a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs
index 45b0282..6da551c 100644
--- a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs
+++ b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSsl.cs
@@ -17,22 +17,42 @@ namespace PKISharp.WACS.Plugins.StorePlugins
private readonly string _path;
private readonly string? _password;
+ public static string? DefaultPath(ISettingsService settings)
+ {
+ var ret = settings.Store.PemFiles?.DefaultPath;
+ if (string.IsNullOrWhiteSpace(ret))
+ {
+ ret = settings.Store.DefaultCentralSslStore;
+ }
+ return ret;
+ }
+
+ public static string? DefaultPassword(ISettingsService settings)
+ {
+ var ret = settings.Store.CentralSsl?.DefaultPassword;
+ if (string.IsNullOrWhiteSpace(ret))
+ {
+ ret = settings.Store.DefaultCentralSslPfxPassword;
+ }
+ return ret;
+ }
+
public CentralSsl(ILogService log, ISettingsService settings, CentralSslOptions options)
{
_log = log;
- _password = !string.IsNullOrWhiteSpace(options.PfxPassword?.Value) ?
- options.PfxPassword.Value :
- settings.Store.DefaultCentralSslPfxPassword;
+ _password = !string.IsNullOrWhiteSpace(options.PfxPassword?.Value) ?
+ options.PfxPassword.Value :
+ DefaultPassword(settings);
- var path = !string.IsNullOrWhiteSpace(options.Path) ?
+ var path = !string.IsNullOrWhiteSpace(options.Path) ?
options.Path :
- settings.Store.DefaultCentralSslStore;
+ DefaultPath(settings);
if (path != null && path.ValidPath(log))
{
_path = path;
- _log.Debug("Using Centralized SSL path: {_path}", _path);
+ _log.Debug("Using CentralSsl path: {_path}", _path);
}
else
{
@@ -44,11 +64,11 @@ namespace PKISharp.WACS.Plugins.StorePlugins
public Task Save(CertificateInfo input)
{
- _log.Information("Copying certificate to the Central SSL store");
+ _log.Information("Copying certificate to the CentralSsl store");
foreach (var identifier in input.SanNames)
{
var dest = PathForIdentifier(identifier);
- _log.Information("Saving certificate to Central SSL location {dest}", dest);
+ _log.Information("Saving certificate to CentralSsl location {dest}", dest);
try
{
var collection = new X509Certificate2Collection
@@ -60,7 +80,7 @@ namespace PKISharp.WACS.Plugins.StorePlugins
}
catch (Exception ex)
{
- _log.Error(ex, "Error copying certificate to Central SSL store");
+ _log.Error(ex, "Error copying certificate to CentralSsl store");
}
}
input.StoreInfo.Add(GetType(),
@@ -74,7 +94,7 @@ namespace PKISharp.WACS.Plugins.StorePlugins
public Task Delete(CertificateInfo input)
{
- _log.Information("Removing certificate from the Central SSL store");
+ _log.Information("Removing certificate from the CentralSsl store");
foreach (var identifier in input.SanNames)
{
var dest = PathForIdentifier(identifier);
diff --git a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptions.cs b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptions.cs
index 9579d47..fb4cbce 100644
--- a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptions.cs
+++ b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptions.cs
@@ -22,7 +22,7 @@ namespace PKISharp.WACS.Plugins.StorePlugins
internal const string PluginName = "CentralSsl";
public override string Name => PluginName;
- public override string Description => "IIS Central Certificate Store (.pfx per domain)";
+ public override string Description => "IIS Central Certificate Store (.pfx per host)";
/// <summary>
/// Show details to the user
diff --git a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptionsFactory.cs b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptionsFactory.cs
index cd8fe09..97aaf95 100644
--- a/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptionsFactory.cs
+++ b/src/main.lib/Plugins/StorePlugins/CentralSsl/CentralSslOptionsFactory.cs
@@ -25,10 +25,10 @@ namespace PKISharp.WACS.Plugins.StorePlugins
var args = _arguments.GetArguments<CentralSslArguments>();
// Get path from command line, default setting or user input
- var path = args.CentralSslStore;
+ var path = args?.CentralSslStore;
if (string.IsNullOrWhiteSpace(path))
{
- path = _settings.Store.DefaultCentralSslStore;
+ path = CentralSsl.DefaultPath(_settings);
}
while (string.IsNullOrWhiteSpace(path) || !path.ValidPath(_log))
{
@@ -36,36 +36,36 @@ namespace PKISharp.WACS.Plugins.StorePlugins
}
// Get password from command line, default setting or user input
- var password = args.PfxPassword;
+ var password = args?.PfxPassword;
if (string.IsNullOrWhiteSpace(password))
{
- password = _settings.Store.DefaultCentralSslPfxPassword;
+ password = CentralSsl.DefaultPassword(_settings);
}
if (string.IsNullOrEmpty(password))
{
- password = await input.ReadPassword("Password to use for the PFX files, or enter for none");
+ password = await input.ReadPassword("Password to use for the .pfx files, or <Enter> for none");
}
- return Create(path, password, args.KeepExisting);
+ return Create(path, password, args?.KeepExisting ?? false);
}
public override async Task<CentralSslOptions?> Default()
{
var args = _arguments.GetArguments<CentralSslArguments>();
- var path = _settings.Store.DefaultCentralSslStore;
+ var path = CentralSsl.DefaultPath(_settings);
if (string.IsNullOrWhiteSpace(path))
{
- path = _arguments.TryGetRequiredArgument(nameof(args.CentralSslStore), args.CentralSslStore);
+ path = _arguments.TryGetRequiredArgument(nameof(args.CentralSslStore), args?.CentralSslStore);
}
- var password = _settings.Store.DefaultCentralSslPfxPassword;
- if (!string.IsNullOrWhiteSpace(args.PfxPassword))
+ var password = CentralSsl.DefaultPassword(_settings);
+ if (!string.IsNullOrWhiteSpace(args?.PfxPassword))
{
password = args.PfxPassword;
}
if (path != null && path.ValidPath(_log))
{
- return Create(path, password, args.KeepExisting);
+ return Create(path, password, args?.KeepExisting ?? false);
}
else
{
@@ -79,11 +79,11 @@ namespace PKISharp.WACS.Plugins.StorePlugins
{
KeepExisting = keepExisting
};
- if (!string.IsNullOrWhiteSpace(password) && !string.Equals(password, _settings.Store.DefaultCentralSslPfxPassword))
+ if (!string.IsNullOrWhiteSpace(password) && !string.Equals(password, CentralSsl.DefaultPassword(_settings)))
{
ret.PfxPassword = new ProtectedString(password);
}
- if (!string.Equals(path, _settings.Store.DefaultCentralSslStore, StringComparison.CurrentCultureIgnoreCase))
+ if (!string.Equals(path, CentralSsl.DefaultPath(_settings), StringComparison.CurrentCultureIgnoreCase))
{
ret.Path = path;
}
diff --git a/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStore.cs b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStore.cs
index 9aa1521..6d2be30 100644
--- a/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStore.cs
+++ b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStore.cs
@@ -48,7 +48,11 @@ namespace PKISharp.WACS.Plugins.StorePlugins
// First priority: specified in the parameters
_storeName = _options.StoreName;
- // Second priority: specified in the .config
+ // Second priority: specified in settings.json
+ if (string.IsNullOrEmpty(_storeName))
+ {
+ _storeName = _settings.Store.CertificateStore?.DefaultStore;
+ }
if (string.IsNullOrEmpty(_storeName))
{
_storeName = _settings.Store.DefaultCertificateStore;
diff --git a/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptionsFactory.cs b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptionsFactory.cs
index de469fa..d534a05 100644
--- a/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptionsFactory.cs
+++ b/src/main.lib/Plugins/StorePlugins/CertificateStore/CertificateStoreOptionsFactory.cs
@@ -21,9 +21,9 @@ namespace PKISharp.WACS.Plugins.StorePlugins
{
var args = _arguments.GetArguments<CertificateStoreArguments>();
var ret = new CertificateStoreOptions {
- StoreName = args.CertificateStore,
- KeepExisting = args.KeepExisting,
- AclFullControl = args.AclFullControl.ParseCsv()
+ StoreName = args?.CertificateStore,
+ KeepExisting = args?.KeepExisting ?? false,
+ AclFullControl = args?.AclFullControl.ParseCsv()
};
return ret;
}
diff --git a/src/main.lib/Plugins/StorePlugins/PemFiles/PemFiles.cs b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFiles.cs
index ffd851a..97f1d86 100644
--- a/src/main.lib/Plugins/StorePlugins/PemFiles/PemFiles.cs
+++ b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFiles.cs
@@ -18,23 +18,35 @@ namespace PKISharp.WACS.Plugins.StorePlugins
private readonly string _path;
+ public static string? DefaultPath(ISettingsService settings)
+ {
+ var ret = settings.Store.PemFiles?.DefaultPath;
+ if (string.IsNullOrWhiteSpace(ret))
+ {
+ ret = settings.Store.DefaultPemFilesPath;
+ }
+ return ret;
+ }
+
public PemFiles(
ILogService log, ISettingsService settings,
PemService pemService, PemFilesOptions options)
{
_log = log;
_pemService = pemService;
- var path = !string.IsNullOrWhiteSpace(options.Path) ?
- options.Path :
- settings.Store.DefaultPemFilesPath;
- if (path != null && path.ValidPath(log))
+ var path = options.Path;
+ if (string.IsNullOrWhiteSpace(path))
{
- _log.Debug("Using .pem certificate path: {path}", path);
+ path = DefaultPath(settings);
+ }
+ if (!string.IsNullOrWhiteSpace(path) && path.ValidPath(log))
+ {
+ _log.Debug("Using .pem files path: {path}", path);
_path = path;
}
else
{
- throw new Exception($"Specified PemFiles path {path} is not valid.");
+ throw new Exception($"Specified .pem files path {path} is not valid.");
}
}
@@ -66,12 +78,15 @@ namespace PKISharp.WACS.Plugins.StorePlugins
// Save complete chain
File.WriteAllText(Path.Combine(_path, $"{name}-chain.pem"), exportString);
- input.StoreInfo.Add(GetType(),
- new StoreInfo()
- {
- Name = PemFilesOptions.PluginName,
- Path = _path
- });
+ if (!input.StoreInfo.ContainsKey(GetType()))
+ {
+ input.StoreInfo.Add(GetType(),
+ new StoreInfo()
+ {
+ Name = PemFilesOptions.PluginName,
+ Path = _path
+ });
+ }
// Private key
if (input.CacheFile != null)
diff --git a/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptionsFactory.cs b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptionsFactory.cs
index 02a26cf..359276a 100644
--- a/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptionsFactory.cs
+++ b/src/main.lib/Plugins/StorePlugins/PemFiles/PemFilesOptionsFactory.cs
@@ -22,10 +22,10 @@ namespace PKISharp.WACS.Plugins.StorePlugins
public override async Task<PemFilesOptions?> Aquire(IInputService input, RunLevel runLevel)
{
var args = _arguments.GetArguments<PemFilesArguments>();
- var path = args.PemFilesPath;
+ var path = args?.PemFilesPath;
if (string.IsNullOrWhiteSpace(path))
{
- path = _settings.Store.DefaultPemFilesPath;
+ path = PemFiles.DefaultPath(_settings);
}
while (string.IsNullOrWhiteSpace(path) || !path.ValidPath(_log))
{
@@ -37,10 +37,10 @@ namespace PKISharp.WACS.Plugins.StorePlugins
public override async Task<PemFilesOptions?> Default()
{
var args = _arguments.GetArguments<PemFilesArguments>();
- var path = _settings.Store.DefaultPemFilesPath;
+ var path = PemFiles.DefaultPath(_settings);
if (string.IsNullOrWhiteSpace(path))
{
- path = _arguments.TryGetRequiredArgument(nameof(args.PemFilesPath), args.PemFilesPath);
+ path = _arguments.TryGetRequiredArgument(nameof(args.PemFilesPath), args?.PemFilesPath);
}
if (path.ValidPath(_log))
{
@@ -55,7 +55,7 @@ namespace PKISharp.WACS.Plugins.StorePlugins
private PemFilesOptions Create(string path)
{
var ret = new PemFilesOptions();
- if (!string.Equals(path, _settings.Store.DefaultPemFilesPath, StringComparison.CurrentCultureIgnoreCase))
+ if (!string.Equals(path, PemFiles.DefaultPath(_settings), StringComparison.CurrentCultureIgnoreCase))
{
ret.Path = path;
}
diff --git a/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFile.cs b/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFile.cs
new file mode 100644
index 0000000..8636eca
--- /dev/null
+++ b/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFile.cs
@@ -0,0 +1,73 @@
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Extensions;
+using PKISharp.WACS.Plugins.Interfaces;
+using PKISharp.WACS.Services;
+using System;
+using System.IO;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+
+namespace PKISharp.WACS.Plugins.StorePlugins
+{
+ internal class PfxFile : IStorePlugin
+ {
+ private readonly ILogService _log;
+ private readonly string _path;
+ private readonly string? _password;
+
+ public PfxFile(ILogService log, ISettingsService settings, PfxFileOptions options)
+ {
+ _log = log;
+
+ _password = !string.IsNullOrWhiteSpace(options.PfxPassword?.Value) ?
+ options.PfxPassword.Value :
+ settings.Store.PfxFile?.DefaultPassword;
+
+ var path = !string.IsNullOrWhiteSpace(options.Path) ?
+ options.Path :
+ settings.Store.PfxFile?.DefaultPath;
+
+ if (path != null && path.ValidPath(log))
+ {
+ _path = path;
+ _log.Debug("Using pfx file path: {_path}", _path);
+ }
+ else
+ {
+ throw new Exception($"Specified pfx file path {path} is not valid.");
+ }
+ }
+
+ private string PathForIdentifier(string identifier) => Path.Combine(_path, $"{identifier.Replace("*", "_")}.pfx");
+
+ public Task Save(CertificateInfo input)
+ {
+ _log.Information("Copying certificate to the pfx folder");
+ var dest = PathForIdentifier(input.CommonName);
+ try
+ {
+ var collection = new X509Certificate2Collection
+ {
+ input.Certificate
+ };
+ collection.AddRange(input.Chain.ToArray());
+ File.WriteAllBytes(dest, collection.Export(X509ContentType.Pfx, _password));
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Error copying certificate to pfx path");
+ }
+ input.StoreInfo.Add(GetType(),
+ new StoreInfo()
+ {
+ Name = PfxFileOptions.PluginName,
+ Path = _path
+ });
+ return Task.CompletedTask;
+ }
+
+ public Task Delete(CertificateInfo input) => Task.CompletedTask;
+
+ (bool, string?) IPlugin.Disabled => (false, null);
+ }
+}
diff --git a/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileArguments.cs b/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileArguments.cs
new file mode 100644
index 0000000..f3ddcbb
--- /dev/null
+++ b/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileArguments.cs
@@ -0,0 +1,8 @@
+namespace PKISharp.WACS.Plugins.StorePlugins
+{
+ internal class PfxFileArguments
+ {
+ public string? PfxFilePath { get; set; }
+ public string? PfxPassword { get; set; }
+ }
+}
diff --git a/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileArgumentsProvider.cs b/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileArgumentsProvider.cs
new file mode 100644
index 0000000..8fac2d0
--- /dev/null
+++ b/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileArgumentsProvider.cs
@@ -0,0 +1,22 @@
+using Fclp;
+using PKISharp.WACS.Configuration;
+
+namespace PKISharp.WACS.Plugins.StorePlugins
+{
+ internal class PfxFileArgumentsProvider : BaseArgumentsProvider<PfxFileArguments>
+ {
+ public override string Name => "PFX file plugin";
+ public override string Group => "Store";
+ public override string Condition => "--store pfxfile";
+
+ public override void Configure(FluentCommandLineParser<PfxFileArguments> parser)
+ {
+ parser.Setup(o => o.PfxFilePath)
+ .As("pfxfilepath")
+ .WithDescription("Path to write the .pfx file to.");
+ parser.Setup(o => o.PfxPassword)
+ .As("pfxpassword")
+ .WithDescription("Password to set for .pfx files exported to the IIS CSS.");
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptions.cs b/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptions.cs
new file mode 100644
index 0000000..aed5bb2
--- /dev/null
+++ b/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptions.cs
@@ -0,0 +1,36 @@
+using PKISharp.WACS.Plugins.Base;
+using PKISharp.WACS.Plugins.Base.Options;
+using PKISharp.WACS.Services;
+using PKISharp.WACS.Services.Serialization;
+
+namespace PKISharp.WACS.Plugins.StorePlugins
+{
+ [Plugin("2a2c576f-7637-4ade-b8db-e8613b0bb33e")]
+ internal class PfxFileOptions : StorePluginOptions<PfxFile>
+ {
+ /// <summary>
+ /// Path to the folder
+ /// </summary>
+ public string? Path { get; set; }
+
+ /// <summary>
+ /// PfxFile password
+ /// </summary>
+ public ProtectedString? PfxPassword { get; set; }
+
+ internal const string PluginName = "PfxFile";
+ public override string Name => PluginName;
+ public override string Description => "PFX archive";
+
+ /// <summary>
+ /// Show details to the user
+ /// </summary>
+ /// <param name="input"></param>
+ public override void Show(IInputService input)
+ {
+ base.Show(input);
+ input.Show("Path", string.IsNullOrEmpty(Path) ? "[Default from settings.json]" : Path, level: 2);
+ input.Show("Password", string.IsNullOrEmpty(PfxPassword?.Value) ? "[Default from settings.json]" : new string('*', PfxPassword.Value.Length), level: 2);
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptionsFactory.cs b/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptionsFactory.cs
new file mode 100644
index 0000000..00359b4
--- /dev/null
+++ b/src/main.lib/Plugins/StorePlugins/PfxFile/PfxFileOptionsFactory.cs
@@ -0,0 +1,91 @@
+using PKISharp.WACS.Extensions;
+using PKISharp.WACS.Plugins.Base.Factories;
+using PKISharp.WACS.Services;
+using PKISharp.WACS.Services.Serialization;
+using System;
+using System.Threading.Tasks;
+
+namespace PKISharp.WACS.Plugins.StorePlugins
+{
+ internal class PfxFileOptionsFactory : StorePluginOptionsFactory<PfxFile, PfxFileOptions>
+ {
+ private readonly ILogService _log;
+ private readonly IArgumentsService _arguments;
+ private readonly ISettingsService _settings;
+
+ public PfxFileOptionsFactory(ILogService log, ISettingsService settings, IArgumentsService arguments)
+ {
+ _log = log;
+ _arguments = arguments;
+ _settings = settings;
+ }
+
+ public override async Task<PfxFileOptions?> Aquire(IInputService input, RunLevel runLevel)
+ {
+ var args = _arguments.GetArguments<PfxFileArguments>();
+
+ // Get path from command line, default setting or user input
+ var path = args?.PfxFilePath;
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ path = _settings.Store.PfxFile?.DefaultPath;
+ }
+ while (string.IsNullOrWhiteSpace(path) || !path.ValidPath(_log))
+ {
+ path = await input.RequestString("Path to folder to store the .pfx file");
+ }
+
+ // Get password from command line, default setting or user input
+ var password = args?.PfxPassword;
+ if (string.IsNullOrWhiteSpace(password))
+ {
+ password = _settings.Store.PfxFile?.DefaultPassword;
+ }
+ if (string.IsNullOrEmpty(password))
+ {
+ password = await input.ReadPassword("Password to use for the .pfx files or <Enter> for none");
+ }
+ return Create(path, password);
+ }
+
+ public override async Task<PfxFileOptions?> Default()
+ {
+ var args = _arguments.GetArguments<PfxFileArguments>();
+ var path = _settings.Store.PfxFile?.DefaultPath;
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ path = _arguments.TryGetRequiredArgument(nameof(args.PfxFilePath), args?.PfxFilePath);
+ }
+
+ var password = _settings.Store.PfxFile?.DefaultPassword;
+ if (!string.IsNullOrWhiteSpace(args?.PfxPassword))
+ {
+ password = args.PfxPassword;
+ }
+
+ if (path != null && path.ValidPath(_log))
+ {
+ return Create(path, password);
+ }
+ else
+ {
+ throw new Exception("Invalid path specified");
+ }
+ }
+
+ private PfxFileOptions Create(string path, string? password)
+ {
+ var ret = new PfxFileOptions();
+ if (!string.IsNullOrWhiteSpace(password) &&
+ !string.Equals(password, _settings.Store.PfxFile?.DefaultPassword))
+ {
+ ret.PfxPassword = new ProtectedString(password);
+ }
+ if (!string.Equals(path, _settings.Store.PfxFile?.DefaultPath, StringComparison.CurrentCultureIgnoreCase))
+ {
+ ret.Path = path;
+ }
+ return ret;
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/TargetPlugins/Csr/CsrOptionsFactory.cs b/src/main.lib/Plugins/TargetPlugins/Csr/CsrOptionsFactory.cs
index a51ac09..b631528 100644
--- a/src/main.lib/Plugins/TargetPlugins/Csr/CsrOptionsFactory.cs
+++ b/src/main.lib/Plugins/TargetPlugins/Csr/CsrOptionsFactory.cs
@@ -25,7 +25,7 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
do
{
ret.CsrFile = await _arguments.TryGetArgument(
- args.CsrFile,
+ args?.CsrFile,
inputService,
"Enter the path to the CSR");
}
@@ -34,9 +34,9 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
string? pkFile;
do
{
- pkFile = await _arguments.TryGetArgument(args.CsrFile,
+ pkFile = await _arguments.TryGetArgument(args?.CsrFile,
inputService,
- "Enter the path to the corresponding private key, or <ENTER> to create a certificate without one");
+ "Enter the path to the corresponding private key, or <Enter> to create a certificate without one");
}
while (!(string.IsNullOrWhiteSpace(pkFile) || pkFile.ValidFile(_log)));
@@ -51,11 +51,11 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
public override async Task<CsrOptions?> Default()
{
var args = _arguments.GetArguments<CsrArguments>();
- if (!args.CsrFile.ValidFile(_log))
+ if (!args?.CsrFile.ValidFile(_log) ?? false)
{
return null;
}
- if (!string.IsNullOrEmpty(args.PkFile))
+ if (!string.IsNullOrEmpty(args?.PkFile))
{
if (!args.PkFile.ValidFile(_log))
{
@@ -64,8 +64,8 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
}
return new CsrOptions()
{
- CsrFile = args.CsrFile,
- PkFile = string.IsNullOrWhiteSpace(args.PkFile) ? null : args.PkFile
+ CsrFile = args?.CsrFile,
+ PkFile = string.IsNullOrWhiteSpace(args?.PkFile) ? null : args.PkFile
};
}
}
diff --git a/src/main.lib/Plugins/TargetPlugins/IIS/IISOptions.cs b/src/main.lib/Plugins/TargetPlugins/IIS/IISOptions.cs
index dea2ec8..3facd2d 100644
--- a/src/main.lib/Plugins/TargetPlugins/IIS/IISOptions.cs
+++ b/src/main.lib/Plugins/TargetPlugins/IIS/IISOptions.cs
@@ -11,7 +11,7 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
internal class IISOptions : TargetPluginOptions<IIS>
{
public override string Name => "IIS";
- public override string Description => "IIS";
+ public override string Description => "Read site bindings from IIS";
/// <summary>
/// Common name for the certificate
diff --git a/src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs b/src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs
index a1afe1e..1ce1d1c 100644
--- a/src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs
+++ b/src/main.lib/Plugins/TargetPlugins/IIS/IISOptionsFactory.cs
@@ -91,16 +91,11 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
{
var allBindings = _iisHelper.GetBindings();
var visibleBindings = allBindings.Where(x => !_arguments.MainArguments.HideHttps || x.Https == false).ToList();
- if (!runLevel.HasFlag(RunLevel.Advanced))
- {
- // Hide bindings with wildcards because they cannot be validated in simple mode
- visibleBindings = visibleBindings.Where(x => !x.Wildcard).ToList();
- }
var ret = await TryAquireSettings(input, allBindings, visibleBindings, allSites, visibleSites, runLevel);
if (ret != null)
{
var filtered = _iisHelper.FilterBindings(allBindings, ret);
- await ListBindings(input, runLevel, filtered, ret.CommonName);
+ await ListBindings(input, filtered, ret.CommonName);
if (await input.PromptYesNo("Continue with this selection?", true))
{
return ret;
@@ -132,9 +127,10 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
List<IISHelper.IISSiteOption> visibleSites,
RunLevel runLevel)
{
+ input.CreateSpace();
input.Show(null, "Please select which website(s) should be scanned for host names. " +
"You may input one or more site identifiers (comma separated) to filter by those sites, " +
- "or alternatively leave the input empty to scan *all* websites.", true);
+ "or alternatively leave the input empty to scan *all* websites.");
var options = new IISOptions();
await input.WritePagedList(
@@ -143,63 +139,62 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
description: $"{x.Name} ({x.Hosts.Count()} binding{(x.Hosts.Count() == 1 ? "" : "s")})",
command: x.Id.ToString(),
color: x.Https ? ConsoleColor.DarkGray : (ConsoleColor?)null)));
- var raw = await input.RequestString("Site identifier(s) or <ENTER> to choose all");
+ var raw = await input.RequestString("Site identifier(s) or <Enter> to choose all");
if (!ParseSiteOptions(raw, allSites, options))
{
return null;
}
var filtered = _iisHelper.FilterBindings(visibleBindings, options);
- await ListBindings(input, runLevel, filtered);
- input.Show(null,
- "You may either choose to include all listed bindings as host names in your certificate, " +
- "or apply an additional filter. Different types of filters are available.", true);
+ await ListBindings(input, filtered);
+ input.CreateSpace();
+ input.Show(null,
+ "Listed above are the bindings found on the selected site(s). By default all of " +
+ "them will be included, but you may either pick specific ones by typing the host names " +
+ "or identifiers (comma seperated) or filter them using one of the options from the " +
+ "menu.");
var askExclude = true;
var filters = new List<Choice<Func<Task>>>
{
Choice.Create<Func<Task>>(() => {
- askExclude = false;
- return InputHosts(
- "Include bindings", input, allBindings, filtered, options,
- () => options.IncludeHosts, x => options.IncludeHosts = x);
- }, "Pick specific bindings from the list"),
- Choice.Create<Func<Task>>(() => {
return InputPattern(input, options);
- }, "Pick bindings based on a search pattern"),
+ }, "Pick bindings based on a search pattern", command: "P"),
Choice.Create<Func<Task>>(() => {
askExclude = false;
return Task.CompletedTask;
- }, "Pick *all* bindings", @default: true)
+ }, "Pick *all* bindings", @default: true, command: "A")
};
if (runLevel.HasFlag(RunLevel.Advanced))
{
- filters.Insert(2, Choice.Create<Func<Task>>(() => {
+ filters.Insert(1, Choice.Create<Func<Task>>(() => {
askExclude = true;
return InputRegex(input, options);
- }, "Pick bindings based on a regular expression"));
+ }, "Pick bindings based on a regular expression", command: "R"));
}
- var chosen = await input.ChooseFromMenu("How do you want to pick the bindings?", filters);
+ var chosen = await input.ChooseFromMenu(
+ "Binding identifiers(s) or menu option",
+ filters,
+ (unknown) =>
+ {
+ return Choice.Create<Func<Task>>(() =>
+ {
+ askExclude = false;
+ return ProcessInputHosts(
+ unknown, allBindings, filtered, options,
+ () => options.IncludeHosts, x => options.IncludeHosts = x);
+ });
+ });
await chosen.Invoke();
filtered = _iisHelper.FilterBindings(allBindings, options);
- // Check for wildcards in simple mode
- if (!runLevel.HasFlag(RunLevel.Advanced) && filtered.Any(x => x.Wildcard))
- {
- await ListBindings(input, runLevel, filtered);
- input.Show(null, "The pattern that you've chosen matches a wildcard binding, which " +
- "is not supported by the 'simple' mode of this program because it requires DNS " +
- "validation. Please try again with a different pattern or use the 'full options' " +
- "mode instead.", true);
- return null;
- }
-
// Exclude specific bindings
if (askExclude && filtered.Count > 1 && runLevel.HasFlag(RunLevel.Advanced))
{
- await ListBindings(input, runLevel, filtered);
+ await ListBindings(input, filtered);
+ input.CreateSpace();
input.Show(null, "The listed bindings match your current filter settings. " +
"If you wish to exclude one or more of them from the certificate, please " +
- "input those bindings now. Press <Enter> to include all listed bindings.", true);
+ "input those bindings now. Press <Enter> to include all listed bindings.");
await InputHosts("Exclude bindings",
input, allBindings, filtered, options,
() => options.ExcludeHosts, x => options.ExcludeHosts = x);
@@ -238,28 +233,51 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
Func<List<string>?> get,
Action<List<string>> set)
{
- var sorted = SortBindings(filtered).ToList();
- var raw = default(string);
+ string? raw;
do
{
raw = await input.RequestString(label);
- if (!string.IsNullOrEmpty(raw))
+ }
+ while (!await ProcessInputHosts(raw, allBindings, filtered, options, get, set));
+ }
+
+ /// <summary>
+ /// Allows users to input both the full host name, or the number that
+ /// it's referred to by in the displayed list
+ /// </summary>
+ /// <param name="label"></param>
+ /// <param name="input"></param>
+ /// <param name="allBindings"></param>
+ /// <param name="filtered"></param>
+ /// <param name="options"></param>
+ /// <param name="get"></param>
+ /// <param name="set"></param>
+ /// <returns></returns>
+ async Task<bool> ProcessInputHosts(
+ string raw,
+ List<IISHelper.IISBindingOption> allBindings,
+ List<IISHelper.IISBindingOption> filtered,
+ IISOptions options,
+ Func<List<string>?> get,
+ Action<List<string>> set)
+ {
+ var sorted = SortBindings(filtered).ToList();
+ if (!string.IsNullOrEmpty(raw))
+ {
+ // Magically replace binding identifiers by their proper host names
+ raw = string.Join(",", raw.ParseCsv().Select(x =>
{
- // Magically replace binding identifiers by their proper host names
- raw = string.Join(",", raw.ParseCsv().Select(x =>
+ if (int.TryParse(x, out var id))
{
- if (int.TryParse(x, out var id))
+ if (id > 0 && id <= sorted.Count())
{
- if (id > 0 && id <= sorted.Count())
- {
- return sorted[id - 1].HostUnicode;
- }
+ return sorted[id - 1].HostUnicode;
}
- return x;
- }));
- }
+ }
+ return x;
+ }));
}
- while (!ParseHostOptions(raw, allBindings, options, get, set));
+ return ParseHostOptions(raw, allBindings, options, get, set);
}
async Task InputCommonName(IInputService input, List<IISHelper.IISBindingOption> filtered, IISOptions options)
@@ -282,7 +300,8 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
/// <returns></returns>
async Task InputPattern(IInputService input, IISOptions options)
{
- input.Show(null, IISArgumentsProvider.PatternExamples, true);
+ input.CreateSpace();
+ input.Show(null, IISArgumentsProvider.PatternExamples);
string raw;
do
{
@@ -358,22 +377,18 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
/// <param name="bindings"></param>
/// <param name="highlight"></param>
/// <returns></returns>
- private async Task ListBindings(IInputService input, RunLevel runLevel, List<IISHelper.IISBindingOption> bindings, string? highlight = null)
+ private async Task ListBindings(IInputService input, List<IISHelper.IISBindingOption> bindings, string? highlight = null)
{
var sortedBindings = SortBindings(bindings);
await input.WritePagedList(
sortedBindings.Select(x => Choice.Create(
item: x,
- color: BindingColor(x, runLevel, highlight))));
+ color: BindingColor(x, highlight))));
}
- private ConsoleColor? BindingColor(IISHelper.IISBindingOption binding, RunLevel runLevel, string? highlight = null)
+ private ConsoleColor? BindingColor(IISHelper.IISBindingOption binding, string? highlight = null)
{
- if (!runLevel.HasFlag(RunLevel.Advanced) && binding.Wildcard)
- {
- return ConsoleColor.Red;
- }
- else if (binding.HostUnicode == highlight)
+ if (binding.HostUnicode == highlight)
{
return ConsoleColor.Green;
}
@@ -406,7 +421,7 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
// be explicit about it.
_log.Error("You have not specified any filters. If you are sure that you want " +
"to create a certificate for *all* bindings on the server, please specific " +
- "-siteid s");
+ "--siteid s");
return null;
}
diff --git a/src/main.lib/Plugins/TargetPlugins/Manual/ManualOptionsFactory.cs b/src/main.lib/Plugins/TargetPlugins/Manual/ManualOptionsFactory.cs
index 363f839..2588b1e 100644
--- a/src/main.lib/Plugins/TargetPlugins/Manual/ManualOptionsFactory.cs
+++ b/src/main.lib/Plugins/TargetPlugins/Manual/ManualOptionsFactory.cs
@@ -27,11 +27,11 @@ namespace PKISharp.WACS.Plugins.TargetPlugins
public override async Task<ManualOptions?> Default()
{
var args = _arguments.GetArguments<ManualArguments>();
- var input = _arguments.TryGetRequiredArgument(nameof(args.Host), args.Host);
+ var input = _arguments.TryGetRequiredArgument(nameof(args.Host), args?.Host);
var ret = Create(input);
if (ret != null)
{
- var commonName = args.CommonName;
+ var commonName = args?.CommonName;
if (!string.IsNullOrWhiteSpace(commonName))
{
commonName = commonName.ToLower().Trim().ConvertPunycode();
diff --git a/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/Acme.cs b/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/Acme.cs
index aa03b6e..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 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));
- 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/Acme/AcmeOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/AcmeOptionsFactory.cs
index b1c2402..35c6390 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/AcmeOptionsFactory.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Dns/Acme/AcmeOptionsFactory.cs
@@ -70,7 +70,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
{
var baseUriRaw =
_arguments.TryGetRequiredArgument(nameof(AcmeArguments.AcmeDnsServer),
- _arguments.GetArguments<AcmeArguments>().AcmeDnsServer);
+ _arguments.GetArguments<AcmeArguments>()?.AcmeDnsServer);
if (!string.IsNullOrEmpty(baseUriRaw))
{
baseUri = new Uri(baseUriRaw);
diff --git a/src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs b/src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs
index 7b8d1c8..267aab4 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Dns/DnsValidation.cs
@@ -1,153 +1,273 @@
-using ACMESharp.Authorizations;
-using PKISharp.WACS.Clients.DNS;
-using PKISharp.WACS.Services;
-using Serilog.Context;
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace PKISharp.WACS.Plugins.ValidationPlugins
-{
- /// <summary>
- /// Base implementation for DNS-01 validation plugins
- /// </summary>reee
- public abstract class DnsValidation<TPlugin> : Validation<Dns01ChallengeValidationDetails>
- {
- protected readonly LookupClientProvider _dnsClient;
- protected readonly ILogService _log;
- protected readonly ISettingsService _settings;
- private string? _recordName;
-
- protected DnsValidation(
- LookupClientProvider dnsClient,
- ILogService log,
- ISettingsService settings)
- {
- _dnsClient = dnsClient;
- _log = log;
- _settings = settings;
- }
-
- public override async Task PrepareChallenge()
- {
- // Check for substitute domains
- if (_settings.Validation.AllowDnsSubstitution)
- {
- try
- {
- // Resolve CNAME in DNS
- var client = await _dnsClient.GetClients(Challenge.DnsRecordName);
- var (_, cname) = await client.First().GetTextRecordValues(Challenge.DnsRecordName, 0);
-
- // Normalize CNAME
- var idn = new IdnMapping();
- cname = cname.ToLower().Trim().TrimEnd('.');
- cname = idn.GetAscii(cname);
-
- // Substitute
- if (cname != Challenge.DnsRecordName)
- {
- _log.Information("Detected that {DnsRecordName} is a CNAME that leads to {cname}", Challenge.DnsRecordName, cname);
- _recordName = cname;
- }
- }
- catch (Exception ex)
- {
- _log.Debug("Error checking for substitute domains: {ex}", ex.Message);
- }
- }
-
- // Create record
- await CreateRecord(_recordName ?? Challenge.DnsRecordName, Challenge.DnsRecordValue);
- _log.Information("Answer should now be available at {answerUri}", _recordName ?? Challenge.DnsRecordName);
-
- // 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 (await PreValidate(retry))
- {
- 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);
- }
- }
- }
- }
-
- protected async Task<bool> PreValidate(int attempt)
- {
- try
- {
- var dnsClients = await _dnsClient.GetClients(Challenge.DnsRecordName, attempt);
- _log.Debug("Looking for TXT value {DnsRecordValue}...", Challenge.DnsRecordValue);
- foreach (var client in dnsClients)
- {
- _log.Debug("Preliminary validation starting from {ip}...", client.IpAddress);
- var (answers, server) = await client.GetTextRecordValues(Challenge.DnsRecordName, attempt);
- _log.Debug("Preliminary validation retrieved answers from {server}", server);
- if (!answers.Any())
- {
- _log.Warning("Preliminary validation failed: no TXT records found");
- return false;
- }
- if (!answers.Contains(Challenge.DnsRecordValue))
- {
- _log.Debug("Preliminary validation found values: {answers}", answers);
- _log.Warning("Preliminary validation failed: incorrect TXT record(s) found");
- return false;
- }
- _log.Debug("Preliminary validation from {ip} looks good", client.IpAddress);
- }
- }
- catch (Exception ex)
- {
- _log.Error(ex, "Preliminary validation failed");
- return false;
- }
- _log.Information("Preliminary validation succeeded");
- return true;
- }
-
- /// <summary>
- /// Delete record when we're done
- /// </summary>
- public override async Task CleanUp()
- {
- if (HasChallenge)
- {
- await DeleteRecord(_recordName ?? Challenge.DnsRecordName, Challenge.DnsRecordValue);;
- }
- }
-
- /// <summary>
- /// Delete validation record
- /// </summary>
- /// <param name="recordName">Name of the record</param>
- public abstract Task DeleteRecord(string recordName, string token);
-
- /// <summary>
- /// Create validation record
- /// </summary>
- /// <param name="recordName">Name of the record</param>
- /// <param name="token">Contents of the record</param>
- public abstract Task CreateRecord(string recordName, string token);
-
- }
-}
+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;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins
+{
+ /// <summary>
+ /// Base implementation for DNS-01 validation plugins
+ /// </summary>reee
+ public abstract class DnsValidation<TPlugin> : Validation<Dns01ChallengeValidationDetails>
+ {
+ protected readonly LookupClientProvider _dnsClient;
+ protected readonly ILogService _log;
+ protected readonly ISettingsService _settings;
+ private readonly List<DnsValidationRecord> _recordsCreated = new List<DnsValidationRecord>();
+
+ protected DnsValidation(
+ LookupClientProvider dnsClient,
+ ILogService log,
+ ISettingsService settings)
+ {
+ _dnsClient = dnsClient;
+ _log = log;
+ _settings = settings;
+ }
+
+ /// <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
+ var authority = await _dnsClient.GetAuthority(
+ challenge.DnsRecordName,
+ followCnames: _settings.Validation.AllowDnsSubstitution);
+
+ var success = false;
+ while (!success)
+ {
+ var record = new DnsValidationRecord(context, authority, challenge.DnsRecordValue);
+ success = await CreateRecord(record);
+ if (!success)
+ {
+ if (authority.From == null)
+ {
+ throw new Exception("Unable to prepare for challenge answer");
+ }
+ else
+ {
+ 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
+ if (_settings.Validation.PreValidateDns)
+ {
+ var validationTasks = _recordsCreated.Select(r => ValidateRecord(r));
+ await Task.WhenAll(validationTasks);
+ }
+ }
+
+ /// <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
+ {
+ _log.Debug("[{identifier}] Looking for TXT value {DnsRecordValue}...", record.Context.Identifier, record.Authority.Domain);
+ foreach (var client in record.Authority.Nameservers)
+ {
+ _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("[{identifier}] Preliminary validation failed: no TXT records found", record.Context.Identifier);
+ return false;
+ }
+ if (!answers.Contains(record.Value))
+ {
+ _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("[{identifier}] Preliminary validation from {ip} looks good", record.Context.Identifier, client.IpAddress);
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "[{identifier}] Preliminary validation failed", record.Context.Identifier);
+ return false;
+ }
+ _log.Information("[{identifier}] Preliminary validation succeeded", record.Context.Identifier);
+ return true;
+ }
+
+ /// <summary>
+ /// Delete record when we're done
+ /// </summary>
+ public override sealed async Task CleanUp()
+ {
+ foreach (var record in _recordsCreated)
+ {
+ try
+ {
+ 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(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(DnsValidationRecord record);
+
+ /// <summary>
+ /// Match DNS zone to use from a list of all zones
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="candidates"></param>
+ /// <param name="recordName"></param>
+ /// <returns></returns>
+ public T? FindBestMatch<T>(Dictionary<string, T> candidates, string recordName) where T: class
+ {
+ var result = candidates.Keys.Select(key =>
+ {
+ var fit = 0;
+ var name = key.TrimEnd('.');
+ if (string.Equals(recordName, name, StringComparison.InvariantCultureIgnoreCase) ||
+ recordName.EndsWith("." + name, StringComparison.InvariantCultureIgnoreCase))
+ {
+ // If there is a zone for a.b.c.com (4) and one for c.com (2)
+ // then the former is a better (more specific) match than the
+ // latter, so we should use that
+ fit = name.Split('.').Count();
+ _log.Verbose("Zone {name} scored {fit} points", key, fit);
+ }
+ else
+ {
+ _log.Verbose("Zone {name} not matched", key);
+ }
+ return new {
+ key,
+ value = candidates[key],
+ fit
+ };
+ }).
+ Where(x => x.fit > 0).
+ OrderByDescending(x => x.fit).
+ FirstOrDefault();
+
+ if (result != null)
+ {
+ _log.Debug("Picked {name} as best match", result.key);
+ return result.value;
+ }
+ else
+ {
+ _log.Error("No match found");
+ 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 317afe9..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,39 +9,42 @@ 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 CreateRecord(string recordName, string token)
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
{
- _input.Show("Domain", _identifier, true);
- _input.Show("Record", recordName);
+ _input.CreateSpace();
+ _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.");
- await _input.Wait("Please press <Enter> after you've created and verified the record");
+ if (!await _input.Wait("Please press <Enter> after you've created and verified the record"))
+ {
+ _log.Warning("User aborted");
+ return false;
+ }
// Pre-pre-validate, allowing the manual user to correct mistakes
while (true)
{
- if (await PreValidate(0))
+ if (await PreValidate(record))
{
- break;
+ return true;
}
else
{
@@ -50,18 +55,19 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
"try ACME validation anyway.", true);
if (!retry)
{
- break;
+ return false;
}
}
}
}
- public override Task DeleteRecord(string recordName, string token)
+ public override Task DeleteRecord(DnsValidationRecord record)
{
- _input.Show("Domain", _identifier, true);
- _input.Show("Record", recordName);
+ _input.CreateSpace();
+ _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 6b7b34b..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 CreateRecord(string recordName, string token)
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
{
var script = _options.Script ?? _options.CreateScript;
if (!string.IsNullOrWhiteSpace(script))
@@ -38,15 +37,24 @@ 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
{
_log.Error("No create script configured");
+ return false;
}
}
- 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))
@@ -56,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
{
@@ -64,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/Dns/Script/ScriptOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Dns/Script/ScriptOptionsFactory.cs
index 3e668c4..c7031a1 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Dns/Script/ScriptOptionsFactory.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Dns/Script/ScriptOptionsFactory.cs
@@ -37,7 +37,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
string? createScript = null;
do
{
- createScript = await _arguments.TryGetArgument(args.DnsCreateScript, input, "Path to script that creates DNS records");
+ createScript = await _arguments.TryGetArgument(args?.DnsCreateScript, input, "Path to script that creates DNS records");
}
while (!createScript.ValidFile(_log));
@@ -53,7 +53,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
Choice.Create<Func<Task>>(async () => {
do {
deleteScript = await _arguments.TryGetArgument(
- args.DnsDeleteScript,
+ args?.DnsDeleteScript,
input,
"Path to script that deletes DNS records");
}
@@ -68,12 +68,12 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
input.Show("{Identifier}", "Domain that's being validated");
input.Show("{RecordName}", "Full TXT record name");
input.Show("{Token}", "Expected value in the TXT record");
- var createArgs = await _arguments.TryGetArgument(args.DnsCreateScriptArguments, input, $"Input parameters for create script, or enter for default \"{Script.DefaultCreateArguments}\"");
+ var createArgs = await _arguments.TryGetArgument(args?.DnsCreateScriptArguments, input, $"Input parameters for create script, or enter for default \"{Script.DefaultCreateArguments}\"");
string? deleteArgs = null;
if (!string.IsNullOrWhiteSpace(ret.DeleteScript) ||
!string.IsNullOrWhiteSpace(ret.Script))
{
- deleteArgs = await _arguments.TryGetArgument(args.DnsDeleteScriptArguments, input, $"Input parameters for delete script, or enter for default \"{Script.DefaultDeleteArguments}\"");
+ deleteArgs = await _arguments.TryGetArgument(args?.DnsDeleteScriptArguments, input, $"Input parameters for delete script, or enter for default \"{Script.DefaultDeleteArguments}\"");
}
ProcessArgs(ret, createArgs, deleteArgs);
return ret;
@@ -83,7 +83,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
{
var args = _arguments.GetArguments<ScriptArguments>();
var ret = new ScriptOptions();
- ProcessScripts(ret, args.DnsScript, args.DnsCreateScript, args.DnsDeleteScript);
+ ProcessScripts(ret, args?.DnsScript, args?.DnsCreateScript, args?.DnsDeleteScript);
if (!string.IsNullOrEmpty(ret.Script))
{
if (!ret.Script.ValidFile(_log))
@@ -109,7 +109,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
}
}
- ProcessArgs(ret, args.DnsCreateScriptArguments, args.DnsDeleteScriptArguments);
+ ProcessArgs(ret, args?.DnsCreateScriptArguments, args?.DnsDeleteScriptArguments);
return ret;
}
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/FileSystem/FileSystemOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemOptionsFactory.cs
index 19a8de3..9a4e748 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemOptionsFactory.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemOptionsFactory.cs
@@ -32,7 +32,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
if (target.IIS && _iisClient.HasWebSites)
{
- if (args.ValidationSiteId != null)
+ if (args?.ValidationSiteId != null)
{
// Throws exception when not found
var site = _iisClient.GetWebSite(args.ValidationSiteId.Value);
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/HttpValidationOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationOptionsFactory.cs
index f98a2e8..99583f4 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationOptionsFactory.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationOptionsFactory.cs
@@ -61,7 +61,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
var args = _arguments.GetArguments<HttpValidationArguments>();
if (string.IsNullOrEmpty(path) && !allowEmpty)
{
- path = _arguments.TryGetRequiredArgument(nameof(args.WebRoot), args.WebRoot);
+ path = _arguments.TryGetRequiredArgument(nameof(args.WebRoot), args?.WebRoot);
}
if (!string.IsNullOrEmpty(path) && !PathIsValid(path))
{
@@ -70,7 +70,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
return new TOptions
{
Path = path,
- CopyWebConfig = target.IIS || args.ManualTargetIsIIS
+ CopyWebConfig = target.IIS || (args?.ManualTargetIsIIS ?? false)
};
}
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 2efacfa..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,41 +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);
- try
- {
- var postfix = "/.well-known/acme-challenge/";
- var prefix = _options.Https == true ?
- $"https://+:{_options.Port ?? DefaultHttpsValidationPort}{postfix}" :
- $"http://+:{_options.Port ?? DefaultHttpValidationPort}{postfix}";
- 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 HttpListener, this may be because of insufficient rights or a non-Microsoft webserver using port 80");
- throw;
+ if (HasListener)
+ {
+ try
+ {
+ Listener.Stop();
+ Listener.Close();
+ }
+ finally
+ {
+ _listener = null;
+ }
+ }
}
+
return Task.CompletedTask;
}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptionsFactory.cs
index 04bb38c..4db84e0 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptionsFactory.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptionsFactory.cs
@@ -26,8 +26,8 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
var args = _arguments.GetArguments<SelfHostingArguments>();
return new SelfHostingOptions()
{
- Port = args.ValidationPort,
- Https = string.Equals(args.ValidationProtocol, "https", StringComparison.OrdinalIgnoreCase) ? true : (bool?)null
+ Port = args?.ValidationPort,
+ Https = string.Equals(args?.ValidationProtocol, "https", StringComparison.OrdinalIgnoreCase) ? true : (bool?)null
};
}
}
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/Tls/SelfHosting/SelfHostingOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Tls/SelfHosting/SelfHostingOptionsFactory.cs
index 451cc19..1d1e436 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Tls/SelfHosting/SelfHostingOptionsFactory.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Tls/SelfHosting/SelfHostingOptionsFactory.cs
@@ -27,7 +27,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Tls
var args = _arguments.GetArguments<SelfHostingArguments>();
return new SelfHostingOptions()
{
- Port = args.ValidationPort
+ Port = args?.ValidationPort
};
}
}
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 ed82881..0210ca2 100644
--- a/src/main.lib/RenewalCreator.cs
+++ b/src/main.lib/RenewalCreator.cs
@@ -25,12 +25,14 @@ namespace PKISharp.WACS
private readonly IAutofacBuilder _scopeBuilder;
private readonly ExceptionHandler _exceptionHandler;
private readonly RenewalExecutor _renewalExecution;
+ private readonly NotificationService _notification;
public RenewalCreator(
PasswordGenerator passwordGenerator, MainArguments args,
IRenewalStore renewalStore, IContainer container,
IInputService input, ILogService log,
ISettingsService settings, IAutofacBuilder autofacBuilder,
+ NotificationService notification,
ExceptionHandler exceptionHandler, RenewalExecutor renewalExecutor)
{
_passwordGenerator = passwordGenerator;
@@ -43,6 +45,7 @@ namespace PKISharp.WACS
_scopeBuilder = autofacBuilder;
_exceptionHandler = exceptionHandler;
_renewalExecution = renewalExecutor;
+ _notification = notification;
}
/// <summary>
@@ -73,7 +76,8 @@ namespace PKISharp.WACS
// it or create it side by side with the current one.
if (runLevel.HasFlag(RunLevel.Interactive))
{
- _input.Show("Existing renewal", existing.ToString(_input), true);
+ _input.CreateSpace();
+ _input.Show("Existing renewal", existing.ToString(_input));
if (!await _input.PromptYesNo($"Overwrite?", true))
{
return temp;
@@ -154,7 +158,7 @@ namespace PKISharp.WACS
}
else if (runLevel.HasFlag(RunLevel.Advanced | RunLevel.Interactive))
{
- var alt = await _input.RequestString($"Suggested friendly name '{initialTarget.FriendlyName}', press <ENTER> to accept or type an alternative");
+ var alt = await _input.RequestString($"Suggested friendly name '{initialTarget.FriendlyName}', press <Enter> to accept or type an alternative");
if (!string.IsNullOrEmpty(alt))
{
tempRenewal.FriendlyName = alt;
@@ -359,11 +363,19 @@ 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
{
- _renewalStore.Save(renewal, result);
+ try
+ {
+ _renewalStore.Save(renewal, result);
+ await _notification.NotifyCreated(renewal, _log.Lines);
+ }
+ catch (Exception ex)
+ {
+ _exceptionHandler.HandleException(ex);
+ }
}
}
diff --git a/src/main.lib/RenewalExecutor.cs b/src/main.lib/RenewalExecutor.cs
index 4dbedbe..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;
@@ -68,6 +51,8 @@ namespace PKISharp.WACS
/// <returns></returns>
public async Task<RenewResult> HandleRenewal(Renewal renewal, RunLevel runLevel)
{
+ _input.CreateSpace();
+ _log.Reset();
using var ts = _scopeBuilder.Target(_container, renewal, runLevel);
using var es = _scopeBuilder.Execution(ts, renewal, runLevel);
// Generate the target
@@ -75,23 +60,23 @@ namespace PKISharp.WACS
var (disabled, disabledReason) = targetPlugin.Disabled;
if (disabled)
{
- throw new Exception($"Target plugin is not available. {disabledReason}");
+ return new RenewResult($"Target plugin is not available. {disabledReason}");
}
var target = await targetPlugin.Generate();
if (target is INull)
{
- throw new Exception($"Target plugin did not generate a target");
+ return new RenewResult($"Target plugin did not generate a target");
}
- if (!target.IsValid(_log))
- {
- throw new Exception($"Target plugin generated an invalid target");
+ if (!target.IsValid(_log))
+ {
+ return new RenewResult($"Target plugin generated an invalid target");
}
// Check if our validation plugin is (still) up to the task
var validationPlugin = es.Resolve<IValidationPluginOptionsFactory>();
if (!validationPlugin.CanValidate(target))
{
- throw new Exception($"Validation plugin is unable to validate the target. A wildcard host was introduced into a HTTP validated renewal.");
+ return new RenewResult($"Validation plugin is unable to validate the target. A wildcard host was introduced into a HTTP validated renewal.");
}
// Create one or more orders based on the target
@@ -99,7 +84,7 @@ namespace PKISharp.WACS
var orders = orderPlugin.Split(renewal, target);
if (orders == null || orders.Count() == 0)
{
- throw new Exception("Order plugin failed to create order(s)");
+ return new RenewResult("Order plugin failed to create order(s)");
}
_log.Verbose("Targeted convert into {n} order(s)", orders.Count());
@@ -187,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)
@@ -198,50 +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);
- }
- }
-
- /// <summary>
/// Steps to take on succesful (re)authorization
/// </summary>
/// <param name="partialTarget"></param>
@@ -294,7 +235,8 @@ namespace PKISharp.WACS
for (var i = 0; i < steps; i++)
{
var storeOptions = context.Renewal.StorePluginOptions[i];
- var storePlugin = (IStorePlugin)context.Scope.Resolve(storeOptions.Instance);
+ var storePlugin = (IStorePlugin)context.Scope.Resolve(storeOptions.Instance,
+ new TypedParameter(storeOptions.GetType(), storeOptions));
if (!(storePlugin is INull))
{
if (steps > 1)
@@ -388,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 66de9cd..af7ba9f 100644
--- a/src/main.lib/RenewalManager.cs
+++ b/src/main.lib/RenewalManager.cs
@@ -67,12 +67,12 @@ namespace PKISharp.WACS
none ? "no renewals" :
$"{selectedRenewals.Count()} of {originalSelection.Count()} {totalLabel}";
+ _input.CreateSpace();
_input.Show(null,
"Welcome to the renewal manager. Actions selected in the menu below will " +
"be applied to the following list of renewals. You may filter the list to target " +
"your action at a more specific set of renewals, or sort it to make it easier to " +
- "find what you're looking for.",
- true);
+ "find what you're looking for.");
var displayRenewals = selectedRenewals;
var displayLimited = !displayAll && selectedRenewals.Count() >= _settings.UI.PageSize;
@@ -201,7 +201,7 @@ namespace PKISharp.WACS
await RevokeCertificates(selectedRenewals);
}
},
- $"Revoke certificate for {selectionLabel}", "V",
+ $"Revoke certificate(s) for {selectionLabel}", "V",
@disabled: (none, "No renewals selected.")));
options.Add(
Choice.Create<Func<Task>>(
@@ -211,7 +211,8 @@ namespace PKISharp.WACS
if (selectedRenewals.Count() > 1)
{
- _input.Show(null, $"Currently selected {selectedRenewals.Count()} of {originalSelection.Count()} {totalLabel}", true);
+ _input.CreateSpace();
+ _input.Show(null, $"Currently selected {selectedRenewals.Count()} of {originalSelection.Count()} {totalLabel}");
}
var chosen = await _input.ChooseFromMenu(
"Choose an action or type numbers to select renewals",
@@ -283,9 +284,10 @@ namespace PKISharp.WACS
$"Select {host.Value.Count()} renewals covering host {host.Key}"));
}
}
+ _input.CreateSpace();
if (options.Count == 0)
{
- _input.Show(null, "Analysis didn't find any overlap between renewals.", first: true);
+ _input.Show(null, "Analysis didn't find any overlap between renewals.");
return selectedRenewals;
}
else
@@ -294,10 +296,9 @@ namespace PKISharp.WACS
Choice.Create(
selectedRenewals.ToList(),
$"Back"));
- _input.Show(null, "Analysis found some overlap between renewals. You can select the overlapping renewals from the menu.", first: true);
+ _input.Show(null, "Analysis found some overlap between renewals. You can select the overlapping renewals from the menu.");
return await _input.ChooseFromMenu("Please choose from the menu", options);
}
-
}
/// <summary>
@@ -395,7 +396,8 @@ namespace PKISharp.WACS
/// <returns></returns>
private async Task<IEnumerable<Renewal>> FilterRenewalsByFriendlyName(IEnumerable<Renewal> current)
{
- _input.Show(null, "Please input friendly name to filter renewals by. " + IISArgumentsProvider.PatternExamples, true);
+ _input.CreateSpace();
+ _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());
@@ -464,7 +466,15 @@ namespace PKISharp.WACS
WarnAboutRenewalArguments();
foreach (var renewal in renewals)
{
- await ProcessRenewal(renewal, runLevel);
+ try
+ {
+ await ProcessRenewal(renewal, runLevel);
+ }
+ catch (Exception ex)
+ {
+ _exceptionHandler.HandleException(ex, "Unhandled error processing renewal");
+ continue;
+ }
}
}
}
@@ -484,18 +494,18 @@ namespace PKISharp.WACS
_renewalStore.Save(renewal, result);
if (result.Success)
{
- notification.NotifySuccess(runLevel, renewal);
+ await notification.NotifySuccess(renewal, _log.Lines);
}
else
{
- notification.NotifyFailure(runLevel, renewal, result.ErrorMessages);
+ await notification.NotifyFailure(runLevel, renewal, result.ErrorMessages, _log.Lines);
}
}
}
catch (Exception ex)
{
_exceptionHandler.HandleException(ex);
- notification.NotifyFailure(runLevel, renewal, new List<string> { ex.Message });
+ await notification.NotifyFailure(runLevel, renewal, new List<string> { ex.Message }, _log.Lines);
}
}
@@ -521,7 +531,8 @@ namespace PKISharp.WACS
{
try
{
- _input.Show("Id", renewal.Id, true);
+ _input.CreateSpace();
+ _input.Show("Id", renewal.Id);
_input.Show("File", $"{renewal.Id}.renewal.json");
_input.Show("FriendlyName", string.IsNullOrEmpty(renewal.FriendlyName) ? $"[Auto] {renewal.LastFriendlyName}" : renewal.FriendlyName);
_input.Show(".pfx password", renewal.PfxPassword?.Value);
@@ -610,7 +621,7 @@ namespace PKISharp.WACS
try
{
await cs.RevokeCertificate(renewal);
- renewal.History.Add(new RenewResult("Certificate revoked"));
+ renewal.History.Add(new RenewResult("Certificate(s) revoked"));
}
catch (Exception ex)
{
diff --git a/src/main.lib/RenewalValidator.cs b/src/main.lib/RenewalValidator.cs
new file mode 100644
index 0000000..704fa3e
--- /dev/null
+++ b/src/main.lib/RenewalValidator.cs
@@ -0,0 +1,446 @@
+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)
+ {
+ 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/ArgumentsParser.cs b/src/main.lib/Services/ArgumentsParser.cs
index 20529f0..ea82ebc 100644
--- a/src/main.lib/Services/ArgumentsParser.cs
+++ b/src/main.lib/Services/ArgumentsParser.cs
@@ -11,7 +11,7 @@ namespace PKISharp.WACS.Configuration
private readonly string[] _args;
private readonly IEnumerable<IArgumentsProvider> _providers;
- public T GetArguments<T>() where T : class, new()
+ public T? GetArguments<T>() where T : class, new()
{
foreach (var provider in _providers)
{
@@ -52,14 +52,18 @@ namespace PKISharp.WACS.Configuration
return false;
}
var mainProvider = _providers.OfType<IArgumentsProvider<MainArguments>>().First();
- if (mainProvider.Validate(_log, main, main))
+ if (mainProvider.Validate(main, main))
{
// Validate the others
var others = _providers.Except(new[] { mainProvider });
foreach (var other in others)
{
var opt = other.GetResult(_args);
- if (!other.Validate(_log, opt, main))
+ if (opt == null)
+ {
+ return false;
+ }
+ if (!other.Validate(opt, main))
{
return false;
}
@@ -84,7 +88,7 @@ namespace PKISharp.WACS.Configuration
foreach (var other in others)
{
var opt = other.GetResult(_args);
- if (other.Active(opt))
+ if (opt != null && other.Active(opt))
{
return true;
}
diff --git a/src/main.lib/Services/ArgumentsService.cs b/src/main.lib/Services/ArgumentsService.cs
index 685ca55..2ec6022 100644
--- a/src/main.lib/Services/ArgumentsService.cs
+++ b/src/main.lib/Services/ArgumentsService.cs
@@ -17,6 +17,10 @@ namespace PKISharp.WACS.Services
if (_mainArguments == null)
{
_mainArguments = _parser.GetArguments<MainArguments>();
+ if (_mainArguments == null)
+ {
+ _mainArguments = new MainArguments();
+ }
}
return _mainArguments;
}
@@ -28,7 +32,7 @@ namespace PKISharp.WACS.Services
_parser = parser;
}
- public T GetArguments<T>() where T : class, new() => _parser.GetArguments<T>();
+ public T? GetArguments<T>() where T : class, new() => _parser.GetArguments<T>();
public async Task<string?> TryGetArgument(string? providedValue, IInputService input, string what, bool secret = false) => await TryGetArgument(providedValue, input, new[] { what }, secret);
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/CertificateService.cs b/src/main.lib/Services/CertificateService.cs
index eca6861..949c9f5 100644
--- a/src/main.lib/Services/CertificateService.cs
+++ b/src/main.lib/Services/CertificateService.cs
@@ -1,529 +1,536 @@
-using ACMESharp.Protocol;
-using Newtonsoft.Json;
-using PKISharp.WACS.Clients.Acme;
-using PKISharp.WACS.Configuration;
-using PKISharp.WACS.DomainObjects;
-using PKISharp.WACS.Extensions;
-using PKISharp.WACS.Plugins.Interfaces;
-using PKISharp.WACS.Services.Serialization;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using System.Threading.Tasks;
-using bc = Org.BouncyCastle;
-
-namespace PKISharp.WACS.Services
-{
- internal class CertificateService : ICertificateService
- {
- private const string CsrPostFix = "-csr.pem";
- private const string PfxPostFix = "-temp.pfx";
- private const string PfxPostFixLegacy = "-cache.pfx";
-
- private readonly IInputService _inputService;
- private readonly ILogService _log;
- private readonly ISettingsService _settings;
- private readonly AcmeClient _client;
- private readonly DirectoryInfo _cache;
- private readonly PemService _pemService;
-
- public CertificateService(
- ILogService log,
- AcmeClient client,
- PemService pemService,
- IInputService inputService,
- ISettingsService settingsService)
- {
- _log = log;
- _client = client;
- _pemService = pemService;
- _cache = new DirectoryInfo(settingsService.Cache.Path);
- _settings = settingsService;
- _inputService = inputService;
- CheckStaleFiles();
- }
-
- /// <summary>
- /// List all files older than 120 days from the certificate
- /// cache, because that means that the certificates have been
- /// expired for 30 days. User might want to clean them up
- /// </summary>
- private void CheckStaleFiles()
- {
- var days = 120;
- var files = _cache.
- GetFiles().
- Where(x => x.LastWriteTime < DateTime.Now.AddDays(-days));
- var count = files.Count();
- if (count > 0)
- {
- _log.Warning("Found {nr} files older than {days} days in cache path '{cachePath}'", count, days, _cache.FullName);
- if (_settings.Cache.DeleteStaleFiles)
- {
- _log.Information("Deleting stale files");
- try
- {
- foreach (var file in files)
- {
- file.Delete();
- }
- _log.Information("Stale files deleted");
- }
- catch (Exception ex)
- {
- _log.Error(ex, "Deleting stale files");
- }
- }
- }
- }
-
- /// <summary>
- /// Delete cached files related to a specific renewal
- /// </summary>
- /// <param name="renewal"></param>
- private void ClearCache(Renewal renewal, string prefix = "*", string postfix = "*")
- {
- foreach (var f in _cache.EnumerateFiles($"{prefix}{renewal.Id}{postfix}"))
- {
- _log.Verbose("Deleting {file} from {folder}", f.Name, _cache.FullName);
- try
- {
- f.Delete();
- }
- catch (Exception ex)
- {
- _log.Warning("Error deleting {file} from {folder}: {message}", f.Name, _cache.FullName, ex.Message);
- }
- }
- }
- void ICertificateService.Delete(Renewal renewal) => ClearCache(renewal);
-
- /// <summary>
- /// Encrypt or decrypt the cached private keys
- /// </summary>
- public void Encrypt()
- {
- foreach (var f in _cache.EnumerateFiles($"*.keys"))
- {
- var x = new ProtectedString(File.ReadAllText(f.FullName), _log);
- _log.Information("Rewriting {x}", f.Name);
- File.WriteAllText(f.FullName, x.DiskValue(_settings.Security.EncryptConfig));
- }
- }
-
- /// <summary>
- /// Find local certificate file based on naming conventions
- /// </summary>
- /// <param name="renewal"></param>
- /// <param name="postfix"></param>
- /// <param name="prefix"></param>
- /// <returns></returns>
- private string GetPath(Renewal renewal, string postfix, string prefix = "") => Path.Combine(_cache.FullName, $"{prefix}{renewal.Id}{postfix}");
-
- /// <summary>
- /// Read from the disk cache
- /// </summary>
- /// <param name="renewal"></param>
- /// <returns></returns>
- public CertificateInfo? CachedInfo(Order order)
- {
- var cachedInfos = CachedInfos(order.Renewal);
- if (!cachedInfos.Any())
- {
- return null;
- }
-
- var keyName = GetPath(order.Renewal, $"-{CacheKey(order)}{PfxPostFix}");
- var fileCache = cachedInfos.Where(x => x.CacheFile?.FullName == keyName).FirstOrDefault();
- if (fileCache == null)
- {
- var legacyFile = GetPath(order.Renewal, PfxPostFixLegacy);
- var candidate = cachedInfos.Where(x => x.CacheFile?.FullName == legacyFile).FirstOrDefault();
- if (candidate != null)
- {
- if (Match(candidate, order.Target))
- {
- fileCache = candidate;
- }
- }
- }
- return fileCache;
- }
-
- public IEnumerable<CertificateInfo> CachedInfos(Renewal renewal)
- {
- var ret = new List<CertificateInfo>();
- var nameAll = GetPath(renewal, "*.pfx");
- var directory = new DirectoryInfo(Path.GetDirectoryName(nameAll));
- var allPattern = Path.GetFileName(nameAll);
- var allFiles = directory.EnumerateFiles(allPattern + "*");
- var fileCache = allFiles.OrderByDescending(x => x.LastWriteTime);
- foreach (var file in fileCache)
- {
- try
- {
- ret.Add(FromCache(file, renewal.PfxPassword?.Value));
- }
- catch
- {
- // File corrupt or invalid password?
- _log.Warning("Unable to read {i} from certificate cache", file.Name);
- }
- }
- return ret;
- }
-
- /// <summary>
- /// See if the information in the certificate matches
- /// that of the specified target. Used to figure out whether
- /// or not the cache is out of date.
- /// </summary>
- /// <param name="target"></param>
- /// <returns></returns>
- private bool Match(CertificateInfo info, Target target)
- {
- var identifiers = target.GetHosts(false);
- var idn = new IdnMapping();
- return info.CommonName == idn.GetAscii(target.CommonName) &&
- info.SanNames.Count == identifiers.Count() &&
- info.SanNames.All(h => identifiers.Contains(idn.GetAscii(h)));
- }
-
- /// <summary>
- /// To check if it's possible to reuse a previously retrieved
- /// certificate we create a hash of its key properties and included
- /// that hash in the file name. If we get the same hash on a
- /// subsequent run, it means it's safe to reuse (no relevant changes).
- /// </summary>
- /// <param name="renewal"></param>
- /// <param name="target"></param>
- /// <returns></returns>
- public string CacheKey(Order order)
- {
- // Check if we can reuse a cached certificate and/or order
- // based on currently active set of parameters and shape of
- // the target.
- var cacheKeyBuilder = new StringBuilder();
- cacheKeyBuilder.Append(order.CacheKeyPart);
- cacheKeyBuilder.Append(order.Target.CommonName);
- cacheKeyBuilder.Append(string.Join(',', order.Target.GetHosts(true).OrderBy(x => x).Select(x => x.ToLower())));
- _ = order.Target.CsrBytes != null ?
- cacheKeyBuilder.Append(Convert.ToBase64String(order.Target.CsrBytes)) :
- cacheKeyBuilder.Append("-");
- _ = order.Renewal.CsrPluginOptions != null ?
- cacheKeyBuilder.Append(JsonConvert.SerializeObject(order.Renewal.CsrPluginOptions)) :
- cacheKeyBuilder.Append("-");
- return cacheKeyBuilder.ToString().SHA1();
- }
-
- /// <summary>
- /// Request certificate from the ACME server
- /// </summary>
- /// <param name="csrPlugin">Plugin used to generate CSR if it has not been provided in the target</param>
- /// <param name="runLevel"></param>
- /// <param name="renewal"></param>
- /// <param name="target"></param>
- /// <param name="order"></param>
- /// <returns></returns>
- public async Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, RunLevel runLevel, Order order)
- {
- if (order.Details == null)
- {
- throw new InvalidOperationException();
- }
-
- // What are we going to get?
- var cacheKey = CacheKey(order);
- var pfxFileInfo = new FileInfo(GetPath(order.Renewal, $"-{cacheKey}{PfxPostFix}"));
-
- // Determine/check the common name
- var identifiers = order.Target.GetHosts(false);
- var commonNameUni = order.Target.CommonName;
- var commonNameAscii = string.Empty;
- if (!string.IsNullOrWhiteSpace(commonNameUni))
- {
- var idn = new IdnMapping();
- commonNameAscii = idn.GetAscii(commonNameUni);
- if (!identifiers.Contains(commonNameAscii, StringComparer.InvariantCultureIgnoreCase))
- {
- _log.Warning($"Common name {commonNameUni} provided is invalid.");
- commonNameAscii = identifiers.First();
- commonNameUni = idn.GetUnicode(commonNameAscii);
- }
- }
-
- // Determine the friendly name base (for the renewal)
- var friendlyNameBase = order.Renewal.FriendlyName;
- if (string.IsNullOrEmpty(friendlyNameBase))
- {
- friendlyNameBase = order.Target.FriendlyName;
- }
- if (string.IsNullOrEmpty(friendlyNameBase))
- {
- friendlyNameBase = commonNameUni;
- }
-
- // Determine the friendly name for this specific certificate
- var friendlyNameIntermediate = friendlyNameBase;
- if (!string.IsNullOrEmpty(order.FriendlyNamePart))
- {
- friendlyNameIntermediate += $" [{order.FriendlyNamePart}]";
- }
- var friendlyName = $"{friendlyNameIntermediate} @ {_inputService.FormatDate(DateTime.Now)}";
-
- // Try using cached certificate first to avoid rate limiting during
- // (initial?) deployment troubleshooting. Real certificate requests
- // will only be done once per day maximum unless the --force parameter
- // is used.
- var cache = CachedInfo(order);
- if (cache != null && cache.CacheFile != null)
- {
- if (cache.CacheFile.LastWriteTime > DateTime.Now.AddDays(_settings.Cache.ReuseDays * -1))
- {
- if (runLevel.HasFlag(RunLevel.IgnoreCache))
- {
- _log.Warning("Cached certificate available on disk but not used due to --{switch} switch.",
- nameof(MainArguments.Force).ToLower());
- }
- else
- {
- _log.Warning("Using cached certificate for {friendlyName}. To force a new request of the " +
- "certificate within {days} days, run with the --{switch} switch.",
- friendlyNameIntermediate,
- _settings.Cache.ReuseDays,
- nameof(MainArguments.Force).ToLower());
- return cache;
- }
- }
- }
-
- if (order.Details.Payload.Status != AcmeClient.OrderValid)
- {
- // Clear cache and write new cert
- ClearCache(order.Renewal, postfix: CsrPostFix);
-
- if (order.Target.CsrBytes == null)
- {
- if (csrPlugin == null)
- {
- throw new InvalidOperationException("Missing csrPlugin");
- }
- var keyFile = GetPath(order.Renewal, ".keys");
- var csr = await csrPlugin.GenerateCsr(keyFile, commonNameAscii, identifiers);
- var keySet = await csrPlugin.GetKeys();
- order.Target.CsrBytes = csr.GetDerEncoded();
- order.Target.PrivateKey = keySet.Private;
- var csrPath = GetPath(order.Renewal, CsrPostFix);
- File.WriteAllText(csrPath, _pemService.GetPem("CERTIFICATE REQUEST", order.Target.CsrBytes));
- _log.Debug("CSR stored at {path} in certificate cache folder {folder}", Path.GetFileName(csrPath), Path.GetDirectoryName(csrPath));
-
- }
-
- _log.Verbose("Submitting CSR");
- order.Details = await _client.SubmitCsr(order.Details, order.Target.CsrBytes);
- if (order.Details.Payload.Status != AcmeClient.OrderValid)
- {
- _log.Error("Unexpected order status {status}", order.Details.Payload.Status);
- throw new Exception($"Unable to complete order");
- }
- }
-
- _log.Information("Requesting certificate {friendlyName}", friendlyNameIntermediate);
- var rawCertificate = await _client.GetCertificate(order.Details);
- if (rawCertificate == null)
- {
- throw new Exception($"Unable to get certificate");
- }
-
- // Build pfx archive including any intermediates provided
- var text = Encoding.UTF8.GetString(rawCertificate);
- var pfx = new bc.Pkcs.Pkcs12Store();
- var startIndex = 0;
- var endIndex = 0;
- const string startString = "-----BEGIN CERTIFICATE-----";
- const string endString = "-----END CERTIFICATE-----";
- while (true)
- {
- startIndex = text.IndexOf(startString, startIndex);
- if (startIndex < 0)
- {
- break;
- }
- endIndex = text.IndexOf(endString, startIndex);
- if (endIndex < 0)
- {
- break;
- }
- endIndex += endString.Length;
- var pem = text[startIndex..endIndex];
- var bcCertificate = _pemService.ParsePem<bc.X509.X509Certificate>(pem);
- if (bcCertificate != null)
- {
- var bcCertificateEntry = new bc.Pkcs.X509CertificateEntry(bcCertificate);
- var bcCertificateAlias = startIndex == 0 ?
- friendlyName :
- bcCertificate.SubjectDN.ToString();
- pfx.SetCertificateEntry(bcCertificateAlias, bcCertificateEntry);
-
- // Assume that the first certificate in the reponse is the main one
- // so we associate the private key with that one. Other certificates
- // are intermediates
- if (startIndex == 0 && order.Target.PrivateKey != null)
- {
- var bcPrivateKeyEntry = new bc.Pkcs.AsymmetricKeyEntry(order.Target.PrivateKey);
- pfx.SetKeyEntry(bcCertificateAlias, bcPrivateKeyEntry, new[] { bcCertificateEntry });
- }
- }
- else
- {
- _log.Warning("PEM data from index {0} to {1} could not be parsed as X509Certificate", startIndex, endIndex);
- }
-
- startIndex = endIndex;
- }
-
- var pfxStream = new MemoryStream();
- pfx.Save(pfxStream, null, new bc.Security.SecureRandom());
- pfxStream.Position = 0;
- using var pfxStreamReader = new BinaryReader(pfxStream);
-
- var tempPfx = new X509Certificate2Collection();
- tempPfx.Import(
- pfxStreamReader.ReadBytes((int)pfxStream.Length),
- null,
- X509KeyStorageFlags.MachineKeySet |
- X509KeyStorageFlags.PersistKeySet |
- X509KeyStorageFlags.Exportable);
-
- ClearCache(order.Renewal, postfix: $"*{PfxPostFix}");
- ClearCache(order.Renewal, postfix: $"*{PfxPostFixLegacy}");
- File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value));
- _log.Debug("Certificate written to cache file {path} in certificate cache folder {folder}. It will be " +
- "reused when renewing within {x} day(s) as long as the Target and Csr parameters remain the same and " +
- "the --force switch is not used.",
- pfxFileInfo.Name,
- pfxFileInfo.Directory.FullName,
- _settings.Cache.ReuseDays);
-
- if (csrPlugin != null)
- {
- try
- {
- var cert = tempPfx.
- OfType<X509Certificate2>().
- Where(x => x.HasPrivateKey).
- FirstOrDefault();
- if (cert != null)
- {
- var certIndex = tempPfx.IndexOf(cert);
- var newVersion = await csrPlugin.PostProcess(cert);
- if (newVersion != cert)
- {
- newVersion.FriendlyName = friendlyName;
- tempPfx[certIndex] = newVersion;
- File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value));
- newVersion.Dispose();
- }
- }
- }
- catch (Exception)
- {
- _log.Warning("Private key conversion error.");
- }
- }
-
- pfxFileInfo.Refresh();
-
- // Update LastFriendlyName so that the user sees
- // the most recently issued friendlyName in
- // the WACS GUI
- order.Renewal.LastFriendlyName = friendlyNameBase;
-
- // Recreate X509Certificate2 with correct flags for Store/Install
- return FromCache(pfxFileInfo, order.Renewal.PfxPassword?.Value);
- }
-
- private CertificateInfo FromCache(FileInfo pfxFileInfo, string? password)
- {
- var rawCollection = ReadAsCollection(pfxFileInfo, password);
- var list = rawCollection.OfType<X509Certificate2>().ToList();
- // Get first certificate that has not been used to issue
- // another one in the collection. That is the outermost leaf.
- var main = list.FirstOrDefault(x => !list.Any(y => x.Subject == y.Issuer));
- list.Remove(main);
- var lastChainElement = main;
- var orderedCollection = new List<X509Certificate2>();
- while (list.Count > 0)
- {
- var signedBy = list.FirstOrDefault(x => main.Issuer == x.Subject);
- if (signedBy == null)
- {
- // Chain cannot be resolved any further
- break;
- }
- orderedCollection.Add(signedBy);
- lastChainElement = signedBy;
- list.Remove(signedBy);
- }
- return new CertificateInfo(main)
- {
- Chain = orderedCollection,
- CacheFile = pfxFileInfo,
- CacheFilePassword = password
- };
- }
-
- /// <summary>
- /// Read certificate for it to be exposed to the StorePlugin and InstallationPlugins
- /// </summary>
- /// <param name="source"></param>
- /// <param name="password"></param>
- /// <returns></returns>
- private X509Certificate2Collection ReadAsCollection(FileInfo source, string? password)
- {
- // Flags used for the X509Certificate2 as
- var externalFlags =
- X509KeyStorageFlags.MachineKeySet |
- X509KeyStorageFlags.PersistKeySet |
- X509KeyStorageFlags.Exportable;
- var ret = new X509Certificate2Collection();
- ret.Import(source.FullName, password, externalFlags);
- return ret;
- }
-
- /// <summary>
- /// Revoke previously issued certificate
- /// </summary>
- /// <param name="binding"></param>
- public async Task RevokeCertificate(Renewal renewal)
- {
- // Delete cached files
- var infos = CachedInfos(renewal);
- foreach (var info in infos)
- {
- try
- {
- var certificateDer = info.Certificate.Export(X509ContentType.Cert);
- await _client.RevokeCertificate(certificateDer);
- info.CacheFile?.Delete();
- _log.Warning($"Revoked certificate {info.Certificate.FriendlyName}");
- }
- catch (Exception ex)
- {
- _log.Error(ex, $"Error revoking certificate {info.Certificate.FriendlyName}, you may retry");
- }
- }
- }
-
- /// <summary>
- /// Common filter for different store plugins
- /// </summary>
- /// <param name="friendlyName"></param>
- /// <returns></returns>
- public static Func<X509Certificate2, bool> ThumbprintFilter(string thumbprint) => new Func<X509Certificate2, bool>(x => string.Equals(x.Thumbprint, thumbprint));
- }
-}
+using Newtonsoft.Json;
+using PKISharp.WACS.Clients.Acme;
+using PKISharp.WACS.Configuration;
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Extensions;
+using PKISharp.WACS.Plugins.Interfaces;
+using PKISharp.WACS.Services.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading.Tasks;
+using bc = Org.BouncyCastle;
+
+namespace PKISharp.WACS.Services
+{
+ internal class CertificateService : ICertificateService
+ {
+ private const string CsrPostFix = "-csr.pem";
+ private const string PfxPostFix = "-temp.pfx";
+ private const string PfxPostFixLegacy = "-cache.pfx";
+
+ private readonly IInputService _inputService;
+ private readonly ILogService _log;
+ private readonly ISettingsService _settings;
+ private readonly AcmeClient _client;
+ private readonly DirectoryInfo _cache;
+ private readonly PemService _pemService;
+
+ public CertificateService(
+ ILogService log,
+ AcmeClient client,
+ PemService pemService,
+ IInputService inputService,
+ ISettingsService settingsService)
+ {
+ _log = log;
+ _client = client;
+ _pemService = pemService;
+ _cache = new DirectoryInfo(settingsService.Cache.Path);
+ _settings = settingsService;
+ _inputService = inputService;
+ CheckStaleFiles();
+ }
+
+ /// <summary>
+ /// List all files older than 120 days from the certificate
+ /// cache, because that means that the certificates have been
+ /// expired for 30 days. User might want to clean them up
+ /// </summary>
+ private void CheckStaleFiles()
+ {
+ var days = 120;
+ var files = _cache.
+ GetFiles().
+ Where(x => x.LastWriteTime < DateTime.Now.AddDays(-days));
+ var count = files.Count();
+ if (count > 0)
+ {
+ _log.Warning("Found {nr} files older than {days} days in cache path '{cachePath}'", count, days, _cache.FullName);
+ if (_settings.Cache.DeleteStaleFiles)
+ {
+ _log.Information("Deleting stale files");
+ try
+ {
+ foreach (var file in files)
+ {
+ file.Delete();
+ }
+ _log.Information("Stale files deleted");
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Deleting stale files");
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Delete cached files related to a specific renewal
+ /// </summary>
+ /// <param name="renewal"></param>
+ private void ClearCache(Renewal renewal, string prefix = "*", string postfix = "*")
+ {
+ foreach (var f in _cache.EnumerateFiles($"{prefix}{renewal.Id}{postfix}"))
+ {
+ if (f.LastWriteTime < DateTime.Now.AddDays(_settings.Cache.ReuseDays * -1))
+ {
+ _log.Verbose("Deleting {file} from {folder}", f.Name, _cache.FullName);
+ try
+ {
+ f.Delete();
+ }
+ catch (Exception ex)
+ {
+ _log.Warning("Error deleting {file} from {folder}: {message}", f.Name, _cache.FullName, ex.Message);
+ }
+ }
+ }
+ }
+ void ICertificateService.Delete(Renewal renewal) => ClearCache(renewal);
+
+ /// <summary>
+ /// Encrypt or decrypt the cached private keys
+ /// </summary>
+ public void Encrypt()
+ {
+ foreach (var f in _cache.EnumerateFiles($"*.keys"))
+ {
+ var x = new ProtectedString(File.ReadAllText(f.FullName), _log);
+ _log.Information("Rewriting {x}", f.Name);
+ File.WriteAllText(f.FullName, x.DiskValue(_settings.Security.EncryptConfig));
+ }
+ }
+
+ /// <summary>
+ /// Find local certificate file based on naming conventions
+ /// </summary>
+ /// <param name="renewal"></param>
+ /// <param name="postfix"></param>
+ /// <param name="prefix"></param>
+ /// <returns></returns>
+ private string GetPath(Renewal renewal, string postfix, string prefix = "") => Path.Combine(_cache.FullName, $"{prefix}{renewal.Id}{postfix}");
+
+ /// <summary>
+ /// Read from the disk cache
+ /// </summary>
+ /// <param name="renewal"></param>
+ /// <returns></returns>
+ public CertificateInfo? CachedInfo(Order order)
+ {
+ var cachedInfos = CachedInfos(order.Renewal);
+ if (!cachedInfos.Any())
+ {
+ return null;
+ }
+
+ var keyName = GetPath(order.Renewal, $"-{CacheKey(order)}{PfxPostFix}");
+ var fileCache = cachedInfos.Where(x => x.CacheFile?.FullName == keyName).FirstOrDefault();
+ if (fileCache == null)
+ {
+ var legacyFile = GetPath(order.Renewal, PfxPostFixLegacy);
+ var candidate = cachedInfos.Where(x => x.CacheFile?.FullName == legacyFile).FirstOrDefault();
+ if (candidate != null)
+ {
+ if (Match(candidate, order.Target))
+ {
+ fileCache = candidate;
+ }
+ }
+ }
+ return fileCache;
+ }
+
+ public IEnumerable<CertificateInfo> CachedInfos(Renewal renewal)
+ {
+ var ret = new List<CertificateInfo>();
+ var nameAll = GetPath(renewal, "*.pfx");
+ var directory = new DirectoryInfo(Path.GetDirectoryName(nameAll));
+ var allPattern = Path.GetFileName(nameAll);
+ var allFiles = directory.EnumerateFiles(allPattern + "*");
+ var fileCache = allFiles.OrderByDescending(x => x.LastWriteTime);
+ foreach (var file in fileCache)
+ {
+ try
+ {
+ ret.Add(FromCache(file, renewal.PfxPassword?.Value));
+ }
+ catch
+ {
+ // File corrupt or invalid password?
+ _log.Warning("Unable to read {i} from certificate cache", file.Name);
+ }
+ }
+ return ret;
+ }
+
+ /// <summary>
+ /// See if the information in the certificate matches
+ /// that of the specified target. Used to figure out whether
+ /// or not the cache is out of date.
+ /// </summary>
+ /// <param name="target"></param>
+ /// <returns></returns>
+ private bool Match(CertificateInfo info, Target target)
+ {
+ var identifiers = target.GetHosts(false);
+ var idn = new IdnMapping();
+ return info.CommonName == idn.GetAscii(target.CommonName) &&
+ info.SanNames.Count == identifiers.Count() &&
+ info.SanNames.All(h => identifiers.Contains(idn.GetAscii(h)));
+ }
+
+ /// <summary>
+ /// To check if it's possible to reuse a previously retrieved
+ /// certificate we create a hash of its key properties and included
+ /// that hash in the file name. If we get the same hash on a
+ /// subsequent run, it means it's safe to reuse (no relevant changes).
+ /// </summary>
+ /// <param name="renewal"></param>
+ /// <param name="target"></param>
+ /// <returns></returns>
+ public string CacheKey(Order order)
+ {
+ // Check if we can reuse a cached certificate and/or order
+ // based on currently active set of parameters and shape of
+ // the target.
+ var cacheKeyBuilder = new StringBuilder();
+ cacheKeyBuilder.Append(order.CacheKeyPart);
+ cacheKeyBuilder.Append(order.Target.CommonName);
+ cacheKeyBuilder.Append(string.Join(',', order.Target.GetHosts(true).OrderBy(x => x).Select(x => x.ToLower())));
+ _ = order.Target.CsrBytes != null ?
+ cacheKeyBuilder.Append(Convert.ToBase64String(order.Target.CsrBytes)) :
+ cacheKeyBuilder.Append("-");
+ _ = order.Renewal.CsrPluginOptions != null ?
+ cacheKeyBuilder.Append(JsonConvert.SerializeObject(order.Renewal.CsrPluginOptions)) :
+ cacheKeyBuilder.Append("-");
+ return cacheKeyBuilder.ToString().SHA1();
+ }
+
+ /// <summary>
+ /// Request certificate from the ACME server
+ /// </summary>
+ /// <param name="csrPlugin">Plugin used to generate CSR if it has not been provided in the target</param>
+ /// <param name="runLevel"></param>
+ /// <param name="renewal"></param>
+ /// <param name="target"></param>
+ /// <param name="order"></param>
+ /// <returns></returns>
+ public async Task<CertificateInfo> RequestCertificate(ICsrPlugin? csrPlugin, RunLevel runLevel, Order order)
+ {
+ if (order.Details == null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ // What are we going to get?
+ var cacheKey = CacheKey(order);
+ var pfxFileInfo = new FileInfo(GetPath(order.Renewal, $"-{cacheKey}{PfxPostFix}"));
+
+ // Determine/check the common name
+ var identifiers = order.Target.GetHosts(false);
+ var commonNameUni = order.Target.CommonName;
+ var commonNameAscii = string.Empty;
+ if (!string.IsNullOrWhiteSpace(commonNameUni))
+ {
+ var idn = new IdnMapping();
+ commonNameAscii = idn.GetAscii(commonNameUni);
+ if (!identifiers.Contains(commonNameAscii, StringComparer.InvariantCultureIgnoreCase))
+ {
+ _log.Warning($"Common name {commonNameUni} provided is invalid.");
+ commonNameAscii = identifiers.First();
+ commonNameUni = idn.GetUnicode(commonNameAscii);
+ }
+ }
+
+ // Determine the friendly name base (for the renewal)
+ var friendlyNameBase = order.Renewal.FriendlyName;
+ if (string.IsNullOrEmpty(friendlyNameBase))
+ {
+ friendlyNameBase = order.Target.FriendlyName;
+ }
+ if (string.IsNullOrEmpty(friendlyNameBase))
+ {
+ friendlyNameBase = commonNameUni;
+ }
+
+ // Determine the friendly name for this specific certificate
+ var friendlyNameIntermediate = friendlyNameBase;
+ if (!string.IsNullOrEmpty(order.FriendlyNamePart))
+ {
+ friendlyNameIntermediate += $" [{order.FriendlyNamePart}]";
+ }
+ var friendlyName = $"{friendlyNameIntermediate} @ {_inputService.FormatDate(DateTime.Now)}";
+
+ // Try using cached certificate first to avoid rate limiting during
+ // (initial?) deployment troubleshooting. Real certificate requests
+ // will only be done once per day maximum unless the --force parameter
+ // is used.
+ var cache = CachedInfo(order);
+ if (cache != null && cache.CacheFile != null)
+ {
+ if (cache.CacheFile.LastWriteTime > DateTime.Now.AddDays(_settings.Cache.ReuseDays * -1))
+ {
+ if (runLevel.HasFlag(RunLevel.IgnoreCache))
+ {
+ _log.Warning("Cached certificate available on disk but not used due to --{switch} switch.",
+ nameof(MainArguments.Force).ToLower());
+ }
+ else
+ {
+ _log.Warning("Using cached certificate for {friendlyName}. To force a new request of the " +
+ "certificate within {days} days, run with the --{switch} switch.",
+ friendlyNameIntermediate,
+ _settings.Cache.ReuseDays,
+ nameof(MainArguments.Force).ToLower());
+ return cache;
+ }
+ }
+ }
+
+ if (order.Details.Payload.Status != AcmeClient.OrderValid)
+ {
+ // Clear cache and write new cert
+ ClearCache(order.Renewal, postfix: CsrPostFix);
+
+ if (order.Target.CsrBytes == null)
+ {
+ if (csrPlugin == null)
+ {
+ throw new InvalidOperationException("Missing csrPlugin");
+ }
+ // Backwards compatible with existing keys, which are not split per order yet.
+ var keyFile = new FileInfo(GetPath(order.Renewal, $".keys"));
+ if (!keyFile.Exists)
+ {
+ keyFile = new FileInfo(GetPath(order.Renewal, $"-{cacheKey}.keys"));
+ }
+ var csr = await csrPlugin.GenerateCsr(keyFile.FullName, commonNameAscii, identifiers);
+ var keySet = await csrPlugin.GetKeys();
+ order.Target.CsrBytes = csr.GetDerEncoded();
+ order.Target.PrivateKey = keySet.Private;
+ var csrPath = GetPath(order.Renewal, $"-{cacheKey}{CsrPostFix}");
+ File.WriteAllText(csrPath, _pemService.GetPem("CERTIFICATE REQUEST", order.Target.CsrBytes));
+ _log.Debug("CSR stored at {path} in certificate cache folder {folder}", Path.GetFileName(csrPath), Path.GetDirectoryName(csrPath));
+
+ }
+
+ _log.Verbose("Submitting CSR");
+ order.Details = await _client.SubmitCsr(order.Details, order.Target.CsrBytes);
+ if (order.Details.Payload.Status != AcmeClient.OrderValid)
+ {
+ _log.Error("Unexpected order status {status}", order.Details.Payload.Status);
+ throw new Exception($"Unable to complete order");
+ }
+ }
+
+ _log.Information("Requesting certificate {friendlyName}", friendlyNameIntermediate);
+ var rawCertificate = await _client.GetCertificate(order.Details);
+ if (rawCertificate == null)
+ {
+ throw new Exception($"Unable to get certificate");
+ }
+
+ // Build pfx archive including any intermediates provided
+ var text = Encoding.UTF8.GetString(rawCertificate);
+ var pfx = new bc.Pkcs.Pkcs12Store();
+ var startIndex = 0;
+ var endIndex = 0;
+ const string startString = "-----BEGIN CERTIFICATE-----";
+ const string endString = "-----END CERTIFICATE-----";
+ while (true)
+ {
+ startIndex = text.IndexOf(startString, startIndex);
+ if (startIndex < 0)
+ {
+ break;
+ }
+ endIndex = text.IndexOf(endString, startIndex);
+ if (endIndex < 0)
+ {
+ break;
+ }
+ endIndex += endString.Length;
+ var pem = text[startIndex..endIndex];
+ var bcCertificate = _pemService.ParsePem<bc.X509.X509Certificate>(pem);
+ if (bcCertificate != null)
+ {
+ var bcCertificateEntry = new bc.Pkcs.X509CertificateEntry(bcCertificate);
+ var bcCertificateAlias = startIndex == 0 ?
+ friendlyName :
+ bcCertificate.SubjectDN.ToString();
+ pfx.SetCertificateEntry(bcCertificateAlias, bcCertificateEntry);
+
+ // Assume that the first certificate in the reponse is the main one
+ // so we associate the private key with that one. Other certificates
+ // are intermediates
+ if (startIndex == 0 && order.Target.PrivateKey != null)
+ {
+ var bcPrivateKeyEntry = new bc.Pkcs.AsymmetricKeyEntry(order.Target.PrivateKey);
+ pfx.SetKeyEntry(bcCertificateAlias, bcPrivateKeyEntry, new[] { bcCertificateEntry });
+ }
+ }
+ else
+ {
+ _log.Warning("PEM data from index {0} to {1} could not be parsed as X509Certificate", startIndex, endIndex);
+ }
+
+ startIndex = endIndex;
+ }
+
+ var pfxStream = new MemoryStream();
+ pfx.Save(pfxStream, null, new bc.Security.SecureRandom());
+ pfxStream.Position = 0;
+ using var pfxStreamReader = new BinaryReader(pfxStream);
+
+ var tempPfx = new X509Certificate2Collection();
+ tempPfx.Import(
+ pfxStreamReader.ReadBytes((int)pfxStream.Length),
+ null,
+ X509KeyStorageFlags.MachineKeySet |
+ X509KeyStorageFlags.PersistKeySet |
+ X509KeyStorageFlags.Exportable);
+
+ ClearCache(order.Renewal, postfix: $"*{PfxPostFix}");
+ ClearCache(order.Renewal, postfix: $"*{PfxPostFixLegacy}");
+ File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value));
+ _log.Debug("Certificate written to cache file {path} in certificate cache folder {folder}. It will be " +
+ "reused when renewing within {x} day(s) as long as the Target and Csr parameters remain the same and " +
+ "the --force switch is not used.",
+ pfxFileInfo.Name,
+ pfxFileInfo.Directory.FullName,
+ _settings.Cache.ReuseDays);
+
+ if (csrPlugin != null)
+ {
+ try
+ {
+ var cert = tempPfx.
+ OfType<X509Certificate2>().
+ Where(x => x.HasPrivateKey).
+ FirstOrDefault();
+ if (cert != null)
+ {
+ var certIndex = tempPfx.IndexOf(cert);
+ var newVersion = await csrPlugin.PostProcess(cert);
+ if (newVersion != cert)
+ {
+ newVersion.FriendlyName = friendlyName;
+ tempPfx[certIndex] = newVersion;
+ File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value));
+ newVersion.Dispose();
+ }
+ }
+ }
+ catch (Exception)
+ {
+ _log.Warning("Private key conversion error.");
+ }
+ }
+
+ pfxFileInfo.Refresh();
+
+ // Update LastFriendlyName so that the user sees
+ // the most recently issued friendlyName in
+ // the WACS GUI
+ order.Renewal.LastFriendlyName = friendlyNameBase;
+
+ // Recreate X509Certificate2 with correct flags for Store/Install
+ return FromCache(pfxFileInfo, order.Renewal.PfxPassword?.Value);
+ }
+
+ private CertificateInfo FromCache(FileInfo pfxFileInfo, string? password)
+ {
+ var rawCollection = ReadAsCollection(pfxFileInfo, password);
+ var list = rawCollection.OfType<X509Certificate2>().ToList();
+ // Get first certificate that has not been used to issue
+ // another one in the collection. That is the outermost leaf.
+ var main = list.FirstOrDefault(x => !list.Any(y => x.Subject == y.Issuer));
+ list.Remove(main);
+ var lastChainElement = main;
+ var orderedCollection = new List<X509Certificate2>();
+ while (list.Count > 0)
+ {
+ var signedBy = list.FirstOrDefault(x => main.Issuer == x.Subject);
+ if (signedBy == null)
+ {
+ // Chain cannot be resolved any further
+ break;
+ }
+ orderedCollection.Add(signedBy);
+ lastChainElement = signedBy;
+ list.Remove(signedBy);
+ }
+ return new CertificateInfo(main)
+ {
+ Chain = orderedCollection,
+ CacheFile = pfxFileInfo,
+ CacheFilePassword = password
+ };
+ }
+
+ /// <summary>
+ /// Read certificate for it to be exposed to the StorePlugin and InstallationPlugins
+ /// </summary>
+ /// <param name="source"></param>
+ /// <param name="password"></param>
+ /// <returns></returns>
+ private X509Certificate2Collection ReadAsCollection(FileInfo source, string? password)
+ {
+ // Flags used for the X509Certificate2 as
+ var externalFlags =
+ X509KeyStorageFlags.MachineKeySet |
+ X509KeyStorageFlags.PersistKeySet |
+ X509KeyStorageFlags.Exportable;
+ var ret = new X509Certificate2Collection();
+ ret.Import(source.FullName, password, externalFlags);
+ return ret;
+ }
+
+ /// <summary>
+ /// Revoke previously issued certificate
+ /// </summary>
+ /// <param name="binding"></param>
+ public async Task RevokeCertificate(Renewal renewal)
+ {
+ // Delete cached files
+ var infos = CachedInfos(renewal);
+ foreach (var info in infos)
+ {
+ try
+ {
+ var certificateDer = info.Certificate.Export(X509ContentType.Cert);
+ await _client.RevokeCertificate(certificateDer);
+ info.CacheFile?.Delete();
+ _log.Warning($"Revoked certificate {info.Certificate.FriendlyName}");
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Error revoking certificate {info.Certificate.FriendlyName}, you may retry");
+ }
+ }
+ }
+
+ /// <summary>
+ /// Common filter for different store plugins
+ /// </summary>
+ /// <param name="friendlyName"></param>
+ /// <returns></returns>
+ public static Func<X509Certificate2, bool> ThumbprintFilter(string thumbprint) => new Func<X509Certificate2, bool>(x => string.Equals(x.Thumbprint, thumbprint));
+ }
+}
diff --git a/src/main.lib/Services/DomainParseService.cs b/src/main.lib/Services/DomainParseService.cs
index a74dfca..17d86e1 100644
--- a/src/main.lib/Services/DomainParseService.cs
+++ b/src/main.lib/Services/DomainParseService.cs
@@ -54,8 +54,8 @@ namespace PKISharp.WACS.Services
}
public string GetTLD(string fulldomain) => Parser.Get(fulldomain).TLD;
- public string GetDomain(string fulldomain) => Parser.Get(fulldomain).Domain;
-
+ public string GetRegisterableDomain(string fulldomain) => Parser.Get(fulldomain).RegistrableDomain;
+
/// <summary>
/// Regular 7 day file cache in the configuration folder
/// </summary>
diff --git a/src/main.lib/Services/InputService.cs b/src/main.lib/Services/InputService.cs
index e4f51a1..829cf5e 100644
--- a/src/main.lib/Services/InputService.cs
+++ b/src/main.lib/Services/InputService.cs
@@ -30,7 +30,7 @@ namespace PKISharp.WACS.Services
}
}
- protected void CreateSpace(bool force = false)
+ public void CreateSpace()
{
if (_log.Dirty || _dirty)
{
@@ -38,9 +38,24 @@ namespace PKISharp.WACS.Services
_dirty = false;
Console.WriteLine();
}
- else if (force)
+ }
+
+ public Task<bool> Continue(string message = "Press <Space> to continue...")
+ {
+ Validate(message);
+ CreateSpace();
+ Console.Write($" {message} ");
+ while (true)
{
- Console.WriteLine();
+ var response = Console.ReadKey(true);
+ switch (response.Key)
+ {
+ case ConsoleKey.Spacebar:
+ Console.SetCursorPosition(0, Console.CursorTop);
+ Console.Write(new string(' ', Console.WindowWidth));
+ Console.SetCursorPosition(0, Console.CursorTop);
+ return Task.FromResult(true);
+ }
}
}
@@ -82,12 +97,8 @@ namespace PKISharp.WACS.Services
return "";
}
- public void Show(string? label, string? value, bool newLine = false, int level = 0)
+ public void Show(string? label, string? value, int level = 0)
{
- if (newLine)
- {
- CreateSpace();
- }
var hasLabel = !string.IsNullOrEmpty(label);
if (hasLabel)
{
@@ -417,7 +428,7 @@ namespace PKISharp.WACS.Services
// Paging
if (currentIndex > 0)
{
- if (await Wait())
+ if (await Continue())
{
currentPage += 1;
}
diff --git a/src/main.lib/Services/Interfaces/IArgumentsProvider.cs b/src/main.lib/Services/Interfaces/IArgumentsProvider.cs
index 58c6a16..a6643ee 100644
--- a/src/main.lib/Services/Interfaces/IArgumentsProvider.cs
+++ b/src/main.lib/Services/Interfaces/IArgumentsProvider.cs
@@ -28,6 +28,11 @@ namespace PKISharp.WACS.Services
bool Default { get; }
/// <summary>
+ /// Reference to the logging service
+ /// </summary>
+ ILogService? Log { get; set; }
+
+ /// <summary>
/// Which options are available
/// </summary>
IEnumerable<ICommandLineOption> Configuration { get; }
@@ -40,7 +45,7 @@ namespace PKISharp.WACS.Services
/// <summary>
/// Get the parsed result
/// </summary>
- object GetResult(string[] args);
+ object? GetResult(string[] args);
/// <summary>
/// Validate against the main arguments
@@ -48,7 +53,7 @@ namespace PKISharp.WACS.Services
/// <param name="current"></param>
/// <param name="main"></param>
/// <returns></returns>
- bool Validate(ILogService log, object current, MainArguments main);
+ bool Validate(object current, MainArguments main);
/// <summary>
/// Are the arguments provided?
@@ -58,12 +63,12 @@ namespace PKISharp.WACS.Services
bool Active(object current);
}
- public interface IArgumentsProvider<T> : IArgumentsProvider where T : new()
+ public interface IArgumentsProvider<T> : IArgumentsProvider where T : class, new()
{
/// <summary>
/// Get the parsed result
/// </summary>
- new T GetResult(string[] args);
+ new T? GetResult(string[] args);
/// <summary>
/// Validate against the main arguments
@@ -71,6 +76,6 @@ namespace PKISharp.WACS.Services
/// <param name="current"></param>
/// <param name="main"></param>
/// <returns></returns>
- bool Validate(ILogService log, T current, MainArguments main);
+ bool Validate(T current, MainArguments main);
}
}
diff --git a/src/main.lib/Services/Interfaces/IArgumentsService.cs b/src/main.lib/Services/Interfaces/IArgumentsService.cs
index 4c00c51..08d2767 100644
--- a/src/main.lib/Services/Interfaces/IArgumentsService.cs
+++ b/src/main.lib/Services/Interfaces/IArgumentsService.cs
@@ -6,7 +6,7 @@ namespace PKISharp.WACS.Services
public interface IArgumentsService
{
MainArguments MainArguments { get; }
- T GetArguments<T>() where T : class, new();
+ T? GetArguments<T>() where T : class, new();
bool Active { get; }
bool Valid { get; }
bool HasFilter();
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/Interfaces/IInputService.cs b/src/main.lib/Services/Interfaces/IInputService.cs
index b59f82c..f911beb 100644
--- a/src/main.lib/Services/Interfaces/IInputService.cs
+++ b/src/main.lib/Services/Interfaces/IInputService.cs
@@ -13,7 +13,8 @@ namespace PKISharp.WACS.Services
Task<string?> ReadPassword(string what);
Task<string> RequestString(string what);
Task<string> RequestString(string[] what);
- void Show(string? label, string? value = null, bool first = false, int level = 0);
+ void CreateSpace();
+ void Show(string? label, string? value = null, int level = 0);
Task<bool> Wait(string message = "Press <Enter> to continue");
Task WritePagedList(IEnumerable<Choice> listItems);
string FormatDate(DateTime date);
diff --git a/src/main.lib/Services/Interfaces/ILogService.cs b/src/main.lib/Services/Interfaces/ILogService.cs
index 9bc182b..e5f060b 100644
--- a/src/main.lib/Services/Interfaces/ILogService.cs
+++ b/src/main.lib/Services/Interfaces/ILogService.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
namespace PKISharp.WACS.Services
{
@@ -10,13 +11,16 @@ namespace PKISharp.WACS.Services
Screen = 1,
Event = 2,
Disk = 4,
- All = Screen | Event | Disk
+ Notification = 8,
+ All = int.MaxValue
}
public interface ILogService
{
bool Dirty { get; set; }
+ IEnumerable<MemoryEntry> Lines { get; }
+ void Reset();
void Debug(string message, params object?[] items);
void Error(Exception ex, string message, params object?[] items);
void Error(string message, params object?[] items);
diff --git a/src/main.lib/Services/Legacy/Importer.cs b/src/main.lib/Services/Legacy/Importer.cs
index 4359d3c..efa06b3 100644
--- a/src/main.lib/Services/Legacy/Importer.cs
+++ b/src/main.lib/Services/Legacy/Importer.cs
@@ -77,13 +77,13 @@ namespace PKISharp.WACS.Services.Legacy
listCommand = "Manage renewals";
renewCommand = "Run";
}
+ _input.CreateSpace();
_input.Show(null,
value: $"The renewals have now been imported into this new version " +
"of the program. Nothing else will happen until new scheduled task is " +
"first run *or* you trigger them manually. It is highly recommended " +
$"to review the imported items with '{listCommand}' and to monitor the " +
- $"results of the first execution with '{renewCommand}'.",
- @first: true);
+ $"results of the first execution with '{renewCommand}'.");
}
diff --git a/src/main.lib/Services/LogService.cs b/src/main.lib/Services/Log/LogService.cs
index e88997d..eb1e429 100644
--- a/src/main.lib/Services/LogService.cs
+++ b/src/main.lib/Services/Log/LogService.cs
@@ -4,8 +4,10 @@ using Serilog.Core;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Runtime.InteropServices;
namespace PKISharp.WACS.Services
@@ -16,10 +18,16 @@ namespace PKISharp.WACS.Services
private readonly Logger? _debugScreenLogger;
private readonly Logger? _eventLogger;
private Logger? _diskLogger;
+ private readonly Logger? _notificationLogger;
private readonly LoggingLevelSwitch _levelSwitch;
+ private readonly List<MemoryEntry> _lines = new List<MemoryEntry>();
+
public bool Dirty { get; set; }
private string _configurationPath { get; }
+ public IEnumerable<MemoryEntry> Lines => _lines.AsEnumerable();
+ public void Reset() => _lines.Clear();
+
public LogService()
{
// Custom configuration support
@@ -82,6 +90,13 @@ namespace PKISharp.WACS.Services
{
Warning("Error creating event logger: {ex}", ex.Message);
}
+
+ _notificationLogger = new LoggerConfiguration()
+ .MinimumLevel.ControlledBy(_levelSwitch)
+ .Enrich.FromLogContext()
+ .WriteTo.Memory(_lines)
+ .CreateLogger();
+
Log.Debug("The global logger has been configured");
}
@@ -181,6 +196,10 @@ namespace PKISharp.WACS.Services
{
_debugScreenLogger.Write(level, ex, message, items);
}
+ if (_notificationLogger != null)
+ {
+ _notificationLogger.Write(level, ex, message, items);
+ }
}
if (_eventLogger != null && type.HasFlag(LogType.Event))
{
@@ -191,5 +210,6 @@ namespace PKISharp.WACS.Services
_diskLogger.Write(level, ex, message, items);
}
}
+
}
}
diff --git a/src/main.lib/Services/Log/MemorySink.cs b/src/main.lib/Services/Log/MemorySink.cs
new file mode 100644
index 0000000..70d2211
--- /dev/null
+++ b/src/main.lib/Services/Log/MemorySink.cs
@@ -0,0 +1,46 @@
+using PKISharp.WACS.Services;
+using Serilog.Configuration;
+using Serilog.Core;
+using Serilog.Events;
+using System;
+using System.Collections.Generic;
+
+namespace PKISharp.WACS.Services
+{
+ public class MemoryEntry
+ {
+ public MemoryEntry(LogEventLevel level, string message)
+ {
+ Level = level;
+ Message = message;
+ }
+
+ public LogEventLevel Level { get; set; }
+ public string Message { get; set; }
+ }
+
+ class MemorySink : ILogEventSink
+ {
+ private readonly IFormatProvider? _formatProvider;
+ private readonly List<MemoryEntry> _list;
+
+ public MemorySink(List<MemoryEntry> list, IFormatProvider? formatProvider = null)
+ {
+ _formatProvider = formatProvider;
+ _list = list;
+ }
+
+ public void Emit(LogEvent logEvent) => _list.Add(new MemoryEntry(logEvent.Level, logEvent.RenderMessage(_formatProvider)));
+ }
+}
+
+namespace Serilog
+{
+ /// <summary>
+ /// Adds the WriteTo.Memory() extension method to <see cref="LoggerConfiguration"/>.
+ /// </summary>
+ public static class LoggerConfigurationStackifyExtensions
+ {
+ public static LoggerConfiguration Memory(this LoggerSinkConfiguration loggerConfiguration, List<MemoryEntry> target, IFormatProvider? formatProvider = null) => loggerConfiguration.Sink(new MemorySink(target, formatProvider));
+ }
+}
diff --git a/src/main.lib/Services/NotificationService.cs b/src/main.lib/Services/NotificationService.cs
index 62e344e..7795fac 100644
--- a/src/main.lib/Services/NotificationService.cs
+++ b/src/main.lib/Services/NotificationService.cs
@@ -1,10 +1,12 @@
using MimeKit;
using PKISharp.WACS.Clients;
using PKISharp.WACS.DomainObjects;
+using Serilog.Events;
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Net.Mail;
+using System.Threading.Tasks;
+using System.Web;
namespace PKISharp.WACS.Services
{
@@ -28,19 +30,47 @@ namespace PKISharp.WACS.Services
}
/// <summary>
+ /// Handle created notification
+ /// </summary>
+ /// <param name="runLevel"></param>
+ /// <param name="renewal"></param>
+ internal async Task NotifyCreated(Renewal renewal, IEnumerable<MemoryEntry> log)
+ {
+ // Do not send emails when running interactively
+ _log.Information(
+ LogType.All,
+ "Certificate {friendlyName} created",
+ renewal.LastFriendlyName);
+ if (_settings.Notification.EmailOnSuccess)
+ {
+ await _email.Send(
+ $"Certificate {renewal.LastFriendlyName} created",
+ @$"<p>Certificate <b>{HttpUtility.HtmlEncode(renewal.LastFriendlyName)}</b> succesfully created.</p>
+ {NotificationInformation(renewal)}
+ {RenderLog(log)}",
+ MessagePriority.Normal);
+ }
+ }
+
+ /// <summary>
/// Handle success notification
/// </summary>
/// <param name="runLevel"></param>
/// <param name="renewal"></param>
- internal void NotifySuccess(RunLevel runLevel, Renewal renewal)
+ internal async Task NotifySuccess(Renewal renewal, IEnumerable<MemoryEntry> log)
{
// Do not send emails when running interactively
- _log.Information(LogType.All, "Renewal for {friendlyName} succeeded", renewal.LastFriendlyName);
- if (runLevel.HasFlag(RunLevel.Unattended) && _settings.Notification.EmailOnSuccess)
+ _log.Information(
+ LogType.All,
+ "Renewal for {friendlyName} succeeded",
+ renewal.LastFriendlyName);
+ if (_settings.Notification.EmailOnSuccess)
{
- _email.Send(
- "Certificate renewal completed",
- $"<p>Certificate <b>{renewal.LastFriendlyName}</b> succesfully renewed.</p> {NotificationInformation(renewal)}",
+ await _email.Send(
+ $"Certificate renewal {renewal.LastFriendlyName} completed",
+ @$"<p>Certificate <b>{HttpUtility.HtmlEncode(renewal.LastFriendlyName)}</b> succesfully renewed.</p>
+ {NotificationInformation(renewal)}
+ {RenderLog(log)}",
MessagePriority.NonUrgent);
}
}
@@ -50,42 +80,79 @@ namespace PKISharp.WACS.Services
/// </summary>
/// <param name="runLevel"></param>
/// <param name="renewal"></param>
- internal void NotifyFailure(RunLevel runLevel, Renewal renewal, List<string> errorMessage)
+ internal async Task NotifyFailure(
+ RunLevel runLevel,
+ Renewal renewal,
+ List<string> errors,
+ IEnumerable<MemoryEntry> log)
{
// Do not send emails when running interactively
_log.Error("Renewal for {friendlyName} failed, will retry on next run", renewal.LastFriendlyName);
- if (errorMessage.Count == 0)
+ if (errors.Count == 0)
{
- errorMessage.Add("No specific error reason provided.");
+ errors.Add("No specific error reason provided.");
}
if (runLevel.HasFlag(RunLevel.Unattended))
{
- _email.Send("Error processing certificate renewal",
- @$"<p>Renewal for <b>{renewal.LastFriendlyName}</b> failed with error(s)
- <ul><li>{string.Join("</li><li>", errorMessage)}</li></ul> will retry
- on next run.</p> {NotificationInformation(renewal)}",
+ 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>
+ {NotificationInformation(renewal)}
+ {RenderLog(log)}",
MessagePriority.Urgent);
}
}
+ private string RenderLog(IEnumerable<MemoryEntry> log) => @$"<p>Log output:<ul><li>{string.Join("</li><li>", log.Select(x => RenderLogEntry(x)))}</ul></p>";
+
+ private string RenderLogEntry(MemoryEntry log)
+ {
+ var color = $"00000";
+ switch (log.Level)
+ {
+ case LogEventLevel.Error:
+ case LogEventLevel.Fatal:
+ color = "#8B0000";
+ break;
+
+ case LogEventLevel.Warning:
+ color = "#CCCC00";
+ break;
+
+ case LogEventLevel.Information:
+ color = "#000000";
+ break;
+
+ case LogEventLevel.Debug:
+ case LogEventLevel.Verbose:
+ color = "#a9a9a9";
+ break;
+ }
+ return $"<span style=\"color:{color}\">{log.Level} - {HttpUtility.HtmlEncode(log.Message)}</span>";
+ }
+
private string NotificationInformation(Renewal renewal)
{
try
{
- var extraMessage = "";
- extraMessage += $"<p>Hosts: {NotificationHosts(renewal)}</p>";
- extraMessage += "<p><table><tr><td>Plugins</td><td></td></tr>";
- extraMessage += $"<tr><td>Target: </td><td> {renewal.TargetPluginOptions.Name}</td></tr>";
- extraMessage += $"<tr><td>Validation: </td><td> {renewal.ValidationPluginOptions.Name}</td></tr>";
+ var extraMessage = @$"<p>
+ <table>
+ <tr><td><b>Hosts</b><td><td></td></tr>
+ <tr><td colspan=""2"">{NotificationHosts(renewal)}</td></tr>
+ <tr><td colspan=""2"">&nbsp;</td></tr>
+ <tr><td><b>Plugins</b></td><td></td></tr>
+ <tr><td>Target: </td><td> {renewal.TargetPluginOptions.Name}</td></tr>";
+ extraMessage += @$"<tr><td>Validation: </td><td> {renewal.ValidationPluginOptions.Name}</td></tr>";
if (renewal.OrderPluginOptions != null)
{
- extraMessage += $"<tr><td>Order: </td><td> {renewal.OrderPluginOptions.Name}</td></tr>";
+ extraMessage += @$"<tr><td>Order: </td><td> {renewal.OrderPluginOptions.Name}</td></tr>";
}
if (renewal.CsrPluginOptions != null)
{
- extraMessage += $"<tr><td>CSR: </td><td> {renewal.CsrPluginOptions.Name}</td></tr>";
+ extraMessage += @$"<tr><td>Csr: </td><td> {renewal.CsrPluginOptions.Name}</td></tr>";
}
- extraMessage += $"<tr><td>Store: </td><td> {string.Join(", ", renewal.StorePluginOptions.Select(x => x.Name))}</td></tr>";
+ extraMessage += @$"<tr><td>Store: </td><td> {string.Join(", ", renewal.StorePluginOptions.Select(x => x.Name))}</td></tr>";
extraMessage += $"<tr><td>Installation: </td><td> {string.Join(", ", renewal.InstallationPluginOptions.Select(x => x.Name))}</td></tr>";
extraMessage += "</table></p>";
return extraMessage;
diff --git a/src/main.lib/Services/PluginService.cs b/src/main.lib/Services/PluginService.cs
index 404e65d..af06227 100644
--- a/src/main.lib/Services/PluginService.cs
+++ b/src/main.lib/Services/PluginService.cs
@@ -17,7 +17,7 @@ namespace PKISharp.WACS.Services
private readonly List<Type> _argumentProviders;
private readonly List<Type> _optionFactories;
private readonly List<Type> _plugins;
-
+
internal readonly ILogService _log;
public IEnumerable<IArgumentsProvider> ArgumentsProviders()
@@ -25,7 +25,9 @@ namespace PKISharp.WACS.Services
return _argumentProviders.Select(x =>
{
var c = x.GetConstructor(new Type[] { });
- return (IArgumentsProvider)c.Invoke(new object[] { });
+ var ret = (IArgumentsProvider)c.Invoke(new object[] { });
+ ret.Log = _log;
+ return ret;
}).ToList();
}
@@ -105,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/ProxyService.cs b/src/main.lib/Services/ProxyService.cs
index f2d49a9..9287ed1 100644
--- a/src/main.lib/Services/ProxyService.cs
+++ b/src/main.lib/Services/ProxyService.cs
@@ -2,6 +2,8 @@
using System.Net;
using System.Net.Http;
using System.Security.Authentication;
+using System.Threading;
+using System.Threading.Tasks;
namespace PKISharp.WACS.Services
{
@@ -29,7 +31,7 @@ namespace PKISharp.WACS.Services
/// <returns></returns>
public HttpClient GetHttpClient(bool checkSsl = true)
{
- var httpClientHandler = new HttpClientHandler()
+ var httpClientHandler = new LoggingHttpClientHandler(_log)
{
Proxy = GetWebProxy(),
SslProtocols = SslProtocols
@@ -45,6 +47,21 @@ namespace PKISharp.WACS.Services
return new HttpClient(httpClientHandler);
}
+ private class LoggingHttpClientHandler : HttpClientHandler
+ {
+ private readonly ILogService _log;
+
+ public LoggingHttpClientHandler(ILogService log) => _log = log;
+
+ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
+ _log.Debug("Send {method} request to {uri}", request.Method, request.RequestUri);
+ var response = await base.SendAsync(request, cancellationToken);
+ _log.Verbose("Request completed with status {s}", response.StatusCode);
+ return response;
+ }
+ }
+
+
/// <summary>
/// Get proxy server to use for web requests
/// </summary>
diff --git a/src/main.lib/Services/RenewalStoreDisk.cs b/src/main.lib/Services/RenewalStoreDisk.cs
index 5527a41..ca35b1d 100644
--- a/src/main.lib/Services/RenewalStoreDisk.cs
+++ b/src/main.lib/Services/RenewalStoreDisk.cs
@@ -34,10 +34,25 @@ namespace PKISharp.WACS.Services
var list = new List<Renewal>();
var di = new DirectoryInfo(_settings.Client.ConfigurationPath);
var postFix = ".renewal.json";
- foreach (var rj in di.EnumerateFiles($"*{postFix}", SearchOption.AllDirectories))
+ var renewalFiles = di.EnumerateFiles($"*{postFix}", SearchOption.AllDirectories);
+ foreach (var rj in renewalFiles)
{
try
{
+ // Just checking if we have write permission
+ using var writeStream = rj.OpenWrite();
+ }
+ catch (Exception ex)
+ {
+ _log.Warning("No write access to all renewals: {reason}", ex.Message);
+ break;
+ }
+ }
+ foreach (var rj in renewalFiles)
+ {
+ try
+ {
+
var storeConverter = new PluginOptionsConverter<StorePluginOptions>(_plugin.PluginOptionTypes<StorePluginOptions>(), _log);
var result = JsonConvert.DeserializeObject<Renewal>(
File.ReadAllText(rj.FullName),
@@ -126,12 +141,19 @@ namespace PKISharp.WACS.Services
var file = RenewalFile(renewal, _settings.Client.ConfigurationPath);
if (file != null)
{
- File.WriteAllText(file.FullName, JsonConvert.SerializeObject(renewal, new JsonSerializerSettings
+ try
{
- NullValueHandling = NullValueHandling.Ignore,
- Formatting = Formatting.Indented,
- Converters = { new ProtectedStringConverter(_log, _settings) }
- }));
+ File.WriteAllText(file.FullName, JsonConvert.SerializeObject(renewal, new JsonSerializerSettings
+ {
+ NullValueHandling = NullValueHandling.Ignore,
+ Formatting = Formatting.Indented,
+ Converters = { new ProtectedStringConverter(_log, _settings) }
+ }));
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Unable to write {renewal} to disk", renewal.LastFriendlyName);
+ }
}
renewal.New = false;
renewal.Updated = false;
diff --git a/src/main.lib/Services/SettingsService.cs b/src/main.lib/Services/SettingsService.cs
index 0fb54c5..e930c96 100644
--- a/src/main.lib/Services/SettingsService.cs
+++ b/src/main.lib/Services/SettingsService.cs
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Configuration;
using PKISharp.WACS.Extensions;
+using PKISharp.WACS.Plugins.StorePlugins;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -484,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>
@@ -551,12 +557,61 @@ namespace PKISharp.WACS.Services
/// </summary>
public string? DefaultStore { get; set; }
+ [Obsolete]
+ public string? DefaultCertificateStore { get; set; }
+ [Obsolete]
+ public string? DefaultCentralSslStore { get; set; }
+ [Obsolete]
+ public string? DefaultCentralSslPfxPassword { get; set; }
+ [Obsolete]
+ public string? DefaultPemFilesPath { get; set; }
+
+ /// <summary>
+ /// Settings for the CentralSsl plugin
+ /// </summary>
+ public CertificateStoreSettings? CertificateStore { get; set; }
+
+ /// <summary>
+ /// Settings for the CentralSsl plugin
+ /// </summary>
+ public CentralSslSettings? CentralSsl { get; set; }
+
+ /// <summary>
+ /// Settings for the PemFiles plugin
+ /// </summary>
+ public PemFilesSettings? PemFiles { get; set; }
+
+ /// <summary>
+ /// Settings for the PfxFile plugin
+ /// </summary>
+ public PfxFileSettings? PfxFile { get; set; }
+
+ }
+
+ public class CertificateStoreSettings
+ {
/// <summary>
/// The certificate store to save the certificates in. If left empty,
/// certificates will be installed either in the WebHosting store,
/// or if that is not available, the My store (better known as Personal).
/// </summary>
- public string? DefaultCertificateStore { get; set; }
+ public string? DefaultStore { get; set; }
+ }
+
+ public class PemFilesSettings
+ {
+ /// <summary>
+ /// When using --store pemfiles this path is used by default, saving
+ /// you the effort from providing it manually. Filling this out makes
+ /// the --pemfilespath parameter unnecessary in most cases. Renewals
+ /// created with the default path will automatically change to any
+ /// future default value, meaning this is also a good practice for
+ /// maintainability.
+ /// </summary>
+ public string? DefaultPath{ get; set; }
+ }
+ public class CentralSslSettings
+ {
/// <summary>
/// When using --store centralssl this path is used by default, saving you
/// the effort from providing it manually. Filling this out makes the
@@ -565,7 +620,7 @@ namespace PKISharp.WACS.Services
/// future default value, meaning this is also a good practice for
/// maintainability.
/// </summary>
- public string? DefaultCentralSslStore { get; set; }
+ public string? DefaultPath { get; set; }
/// <summary>
/// When using --store centralssl this password is used by default for
/// the pfx files, saving you the effort from providing it manually.
@@ -574,16 +629,29 @@ namespace PKISharp.WACS.Services
/// automatically change to any future default value, meaning this
/// is also a good practice for maintainability.
/// </summary>
- public string? DefaultCentralSslPfxPassword { get; set; }
+ public string? DefaultPassword { get; set; }
+ }
+
+ public class PfxFileSettings
+ {
/// <summary>
- /// When using --store pemfiles this path is used by default, saving
+ /// When using --store pfxfile this path is used by default, saving
/// you the effort from providing it manually. Filling this out makes
- /// the --pemfilespath parameter unnecessary in most cases. Renewals
+ /// the --pfxfilepath parameter unnecessary in most cases. Renewals
/// created with the default path will automatically change to any
/// future default value, meaning this is also a good practice for
/// maintainability.
/// </summary>
- public string? DefaultPemFilesPath { get; set; }
+ public string? DefaultPath { get; set; }
+ /// <summary>
+ /// When using --store pfxfile this password is used by default for
+ /// the pfx files, saving you the effort from providing it manually.
+ /// Filling this out makes the --pfxpassword parameter unnecessary in
+ /// most cases. Renewals created with the default password will
+ /// automatically change to any future default value, meaning this
+ /// is also a good practice for maintainability.
+ /// </summary>
+ public string? DefaultPassword { get; set; }
}
public class InstallationSettings
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.lib/Wacs.cs b/src/main.lib/Wacs.cs
index 8aeca85..be8faf9 100644
--- a/src/main.lib/Wacs.cs
+++ b/src/main.lib/Wacs.cs
@@ -235,20 +235,20 @@ namespace PKISharp.WACS.Host
{
var total = _renewalStore.Renewals.Count();
var due = _renewalStore.Renewals.Count(x => x.IsDue());
- var error = _renewalStore.Renewals.Count(x => !x.History.Last().Success);
+ var error = _renewalStore.Renewals.Count(x => !x.History.LastOrDefault()?.Success ?? false);
var (allowIIS, allowIISReason) = _userRoleService.AllowIIS;
var options = new List<Choice<Func<Task>>>
{
Choice.Create<Func<Task>>(
() => _renewalCreator.SetupRenewal(RunLevel.Interactive | RunLevel.Simple),
- "Create new certificate (default settings)", "N",
+ "Create certificate (default settings)", "N",
@default: true),
Choice.Create<Func<Task>>(
- () => _renewalCreator.SetupRenewal(RunLevel.Interactive | RunLevel.Advanced),
- "Create new certificate (full options)", "M"),
+ () => _renewalCreator.SetupRenewal(RunLevel.Interactive | RunLevel.Advanced),
+ "Create certificate (full options)", "M"),
Choice.Create<Func<Task>>(
() => _renewalManager.CheckRenewals(RunLevel.Interactive),
- $"Run scheduled renewals ({due} currently due)", "R",
+ $"Run renewals ({due} currently due)", "R",
color: due == 0 ? (ConsoleColor?)null : ConsoleColor.Yellow),
Choice.Create<Func<Task>>(
() => _renewalManager.ManageRenewals(),
@@ -341,13 +341,15 @@ namespace PKISharp.WACS.Host
"use this tools to temporarily unprotect your data before moving from the old machine. " +
"The renewal files includes passwords for your certificates, other passwords/keys, and a key used " +
"for signing requests for new certificates.");
- _input.Show(null, "To remove machine-dependent protections, use the following steps.", true);
+ _input.CreateSpace();
+ _input.Show(null, "To remove machine-dependent protections, use the following steps.");
_input.Show(null, " 1. On your old machine, set the EncryptConfig setting to false");
_input.Show(null, " 2. Run this option; all protected values will be unprotected.");
_input.Show(null, " 3. Copy your data files to the new machine.");
_input.Show(null, " 4. On the new machine, set the EncryptConfig setting to true");
_input.Show(null, " 5. Run this option; all unprotected values will be saved with protection");
- _input.Show(null, $"Data directory: {settings.Client.ConfigurationPath}", true);
+ _input.CreateSpace();
+ _input.Show(null, $"Data directory: {settings.Client.ConfigurationPath}");
_input.Show(null, $"Config directory: {new FileInfo(settings.ExePath).Directory.FullName}\\settings.json");
_input.Show(null, $"Current EncryptConfig setting: {encryptConfig}");
userApproved = await _input.PromptYesNo($"Save all renewal files {(encryptConfig ? "with" : "without")} encryption?", false);
@@ -378,7 +380,8 @@ namespace PKISharp.WACS.Host
{
throw new InvalidOperationException();
}
- _input.Show("Account ID", acmeAccount.Payload.Id ?? "-", true);
+ _input.CreateSpace();
+ _input.Show("Account ID", acmeAccount.Payload.Id ?? "-");
_input.Show("Created", acmeAccount.Payload.CreatedAt);
_input.Show("Initial IP", acmeAccount.Payload.InitialIp);
_input.Show("Status", acmeAccount.Payload.Status);
diff --git a/src/main.lib/wacs.lib.csproj b/src/main.lib/wacs.lib.csproj
index 1a56301..ddb25ba 100644
--- a/src/main.lib/wacs.lib.csproj
+++ b/src/main.lib/wacs.lib.csproj
@@ -23,17 +23,17 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Autofac" Version="5.1.2" />
- <PackageReference Include="DnsClient" Version="1.3.0" />
- <PackageReference Include="MailKit" Version="2.5.2" />
- <PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.3" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.3" />
- <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="3.1.3" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" />
+ <PackageReference Include="Autofac" Version="5.2.0" />
+ <PackageReference Include="DnsClient" Version="1.3.2" />
+ <PackageReference Include="MailKit" Version="2.7.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.4" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.4" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="3.1.4" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.4" />
<PackageReference Include="Microsoft.Web.Administration" Version="11.1.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
<PackageReference Include="Nager.PublicSuffix" Version="1.5.1" />
- <PackageReference Include="Portable.BouncyCastle" Version="1.8.6" />
+ <PackageReference Include="Portable.BouncyCastle" Version="1.8.6.7" />
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Settings.AppSettings" Version="2.2.2" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
@@ -48,7 +48,7 @@
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.7.0" />
<PackageReference Include="System.Security.Cryptography.X509Certificates" Version="4.3.2" />
<PackageReference Include="TaskScheduler" Version="2.8.18" />
- <PackageReference Include="WebDav.Client" Version="2.4.0" />
+ <PackageReference Include="WebDav.Client" Version="2.6.0" />
</ItemGroup>
<ItemGroup>
@@ -56,4 +56,10 @@
<ProjectReference Include="..\fluent-command-line-parser\FluentCommandLineParser\FluentCommandLineParser.csproj" />
</ItemGroup>
+ <ItemGroup>
+ <Folder Include="Plugins\OrderPlugins\Domain\" />
+ <Folder Include="Plugins\OrderPlugins\Site\" />
+ <Folder Include="Plugins\StorePlugins\PfxFile\" />
+ </ItemGroup>
+
</Project>
diff --git a/src/main.test/Mock/MockContainer.cs b/src/main.test/Mock/MockContainer.cs
new file mode 100644
index 0000000..76560ae
--- /dev/null
+++ b/src/main.test/Mock/MockContainer.cs
@@ -0,0 +1,68 @@
+using Autofac;
+using PKISharp.WACS.Clients;
+using PKISharp.WACS.Clients.Acme;
+using PKISharp.WACS.Clients.DNS;
+using PKISharp.WACS.Clients.IIS;
+using PKISharp.WACS.Configuration;
+using PKISharp.WACS.Plugins.Resolvers;
+using System.Collections.Generic;
+using mock = PKISharp.WACS.UnitTests.Mock.Services;
+using real = PKISharp.WACS.Services;
+
+namespace PKISharp.WACS.UnitTests.Mock
+{
+ class MockContainer
+ {
+ public ILifetimeScope TestScope()
+ {
+ var log = new mock.LogService(false);
+ var pluginService = new real.PluginService(log);
+ var argumentsParser = new ArgumentsParser(log, pluginService, $"".Split(' '));
+ var argumentsService = new real.ArgumentsService(log, argumentsParser);
+ var input = new mock.InputService(new List<string>()
+ {
+ "C", // Cancel command
+ "y", // Confirm cancel all
+ "Q" // Quit
+ });
+
+ var builder = new ContainerBuilder();
+ _ = builder.RegisterInstance(log).As<real.ILogService>();
+ _ = builder.RegisterInstance(argumentsParser).As<ArgumentsParser>();
+ _ = builder.RegisterInstance(argumentsService).As<real.IArgumentsService>();
+ _ = builder.RegisterInstance(argumentsService).As<real.IArgumentsService>();
+ _ = builder.RegisterInstance(pluginService).As<real.IPluginService>();
+ _ = builder.RegisterInstance(input).As<real.IInputService>();
+
+ _ = builder.RegisterType<mock.MockRenewalStore>().As<real.IRenewalStore>().SingleInstance();
+ _ = builder.RegisterType<mock.MockSettingsService>().As<real.ISettingsService>().SingleInstance(); ;
+ _ = builder.RegisterType<mock.UserRoleService>().As<real.IUserRoleService>().SingleInstance();
+ _ = builder.RegisterType<real.ProxyService>().SingleInstance();
+ _ = builder.RegisterType<real.PasswordGenerator>().SingleInstance();
+
+ pluginService.Configure(builder);
+
+ _ = builder.RegisterType<real.DomainParseService>().SingleInstance();
+ _ = builder.RegisterType<Mock.Clients.MockIISClient>().As<IIISClient>().SingleInstance();
+ _ = builder.RegisterType<IISHelper>().SingleInstance();
+ _ = builder.RegisterType<real.ExceptionHandler>().SingleInstance();
+ _ = builder.RegisterType<UnattendedResolver>();
+ _ = builder.RegisterType<InteractiveResolver>();
+ _ = builder.RegisterType<real.AutofacBuilder>().As<real.IAutofacBuilder>().SingleInstance();
+ _ = builder.RegisterType<AcmeClient>().SingleInstance();
+ _ = builder.RegisterType<real.PemService>().SingleInstance();
+ _ = builder.RegisterType<EmailClient>().SingleInstance();
+ _ = builder.RegisterType<ScriptClient>().SingleInstance();
+ _ = builder.RegisterType<LookupClientProvider>().SingleInstance();
+ _ = 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();
+
+ return builder.Build();
+ }
+ }
+}
diff --git a/src/main.test/Mock/Services/InputService.cs b/src/main.test/Mock/Services/InputService.cs
index 8fc5561..328f0f5 100644
--- a/src/main.test/Mock/Services/InputService.cs
+++ b/src/main.test/Mock/Services/InputService.cs
@@ -23,7 +23,7 @@ namespace PKISharp.WACS.UnitTests.Mock.Services
{
return Task.
FromResult(default(TResult));
- }
+ }
else
{
return Task.
@@ -34,7 +34,7 @@ namespace PKISharp.WACS.UnitTests.Mock.Services
}
public Task<TResult> ChooseRequired<TSource, TResult>(
string what,
- IEnumerable<TSource> options,
+ IEnumerable<TSource> options,
Func<TSource, Choice<TResult>> creator)
{
var input = GetNextInput();
@@ -52,7 +52,7 @@ namespace PKISharp.WACS.UnitTests.Mock.Services
public Task<string?> ReadPassword(string what) => Task.FromResult<string?>(GetNextInput());
public Task<string> RequestString(string what) => Task.FromResult(GetNextInput());
public Task<string> RequestString(string[] what) => Task.FromResult(GetNextInput());
- public void Show(string? label, string? value = null, bool first = false, int level = 0) { }
+ public void Show(string? label, string? value = null, int level = 0) { }
public Task<bool> Wait(string message = "") => Task.FromResult(true);
public Task WritePagedList(IEnumerable<Choice> listItems) => Task.CompletedTask;
public Task<TResult> ChooseFromMenu<TResult>(string what, List<Choice<TResult>> choices, Func<string, Choice<TResult>>? unexpected = null)
@@ -69,5 +69,7 @@ namespace PKISharp.WACS.UnitTests.Mock.Services
}
throw new Exception();
}
+
+ public void CreateSpace() { }
}
}
diff --git a/src/main.test/Mock/Services/LogService.cs b/src/main.test/Mock/Services/LogService.cs
index be9ea4c..a2bb11d 100644
--- a/src/main.test/Mock/Services/LogService.cs
+++ b/src/main.test/Mock/Services/LogService.cs
@@ -3,6 +3,7 @@ using Serilog;
using Serilog.Core;
using System;
using System.Collections.Concurrent;
+using System.Collections.Generic;
namespace PKISharp.WACS.UnitTests.Mock.Services
{
@@ -26,6 +27,9 @@ namespace PKISharp.WACS.UnitTests.Mock.Services
}
public bool Dirty { get; set; }
+
+ public IEnumerable<MemoryEntry> Lines => new List<MemoryEntry>();
+
public void Debug(string message, params object?[] items)
{
DebugMessages.Enqueue(message);
@@ -75,5 +79,7 @@ namespace PKISharp.WACS.UnitTests.Mock.Services
WarningMessages.Enqueue(message);
_logger.Warning(message, items);
}
+
+ public void Reset() { }
}
}
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/DnsValidationTests/When_resolving_name_servers.cs b/src/main.test/Tests/DnsValidationTests/When_resolving_name_servers.cs
index 7373f09..b8d6427 100644
--- a/src/main.test/Tests/DnsValidationTests/When_resolving_name_servers.cs
+++ b/src/main.test/Tests/DnsValidationTests/When_resolving_name_servers.cs
@@ -30,26 +30,26 @@ namespace PKISharp.WACS.UnitTests.Tests.DnsValidationTests
public void Should_recursively_follow_cnames(string challengeUri, string expectedToken)
{
//var client = _dnsClient.DefaultClient();
- var client = _dnsClient.GetClients(challengeUri).Result.First();
- var (tokens, cname) = client.GetTextRecordValues(challengeUri, 0).Result;
+ var auth = _dnsClient.GetAuthority(challengeUri).Result;
+ Assert.AreEqual(auth.Domain, "_acme-challenge.logs.hourstrackercloud.com");
+ var tokens = auth.Nameservers.First().GetTxtRecords(challengeUri).Result;
Assert.IsTrue(tokens.Contains(expectedToken));
- Assert.AreEqual(cname, "_acme-challenge.logs.hourstrackercloud.com");
}
[TestMethod]
[DataRow("activesync.dynu.net")]
[DataRow("tweakers.net")]
- public void Should_find_nameserver(string domain) => _ = _dnsClient.GetClients(domain).Result;
+ public void Should_find_nameserver(string domain) => _ = _dnsClient.GetAuthority(domain).Result;
[TestMethod]
[DataRow("_acme-challenge.acmedns.wouter.tinus.online")]
public void Should_Find_Txt(string domain)
{
- var client = _dnsClient.GetClients(domain).Result.First();
- var (tokens, cname) = client.GetTextRecordValues(domain, 0).Result;
+ var auth = _dnsClient.GetAuthority(domain).Result;
+ var tokens = auth.Nameservers.First().GetTxtRecords(auth.Domain).Result;
Assert.IsTrue(tokens.Any());
- Assert.AreEqual(cname, "86af4f7c-b82c-4b7d-a75b-3feafbabbb2e.auth.acme-dns.io.");
+ Assert.AreEqual(auth.Domain, "86af4f7c-b82c-4b7d-a75b-3feafbabbb2e.auth.acme-dns.io");
}
}
}
diff --git a/src/main.test/Tests/EcnryptionTests/EncryptionTest.cs b/src/main.test/Tests/EncryptionTests/EncryptionTest.cs
index d370eb0..d370eb0 100644
--- a/src/main.test/Tests/EcnryptionTests/EncryptionTest.cs
+++ b/src/main.test/Tests/EncryptionTests/EncryptionTest.cs
diff --git a/src/main.test/Tests/InstallationPluginTests/ArgumentParserTests.cs b/src/main.test/Tests/InstallationPluginTests/ArgumentParserTests.cs
index debcad1..7a6110a 100644
--- a/src/main.test/Tests/InstallationPluginTests/ArgumentParserTests.cs
+++ b/src/main.test/Tests/InstallationPluginTests/ArgumentParserTests.cs
@@ -3,6 +3,7 @@ using PKISharp.WACS.Configuration;
using PKISharp.WACS.Plugins.InstallationPlugins;
using PKISharp.WACS.Services;
using PKISharp.WACS.UnitTests.Mock.Services;
+using System;
namespace PKISharp.WACS.UnitTests.Tests.InstallationPluginTests
{
@@ -21,11 +22,12 @@ namespace PKISharp.WACS.UnitTests.Tests.InstallationPluginTests
$"--scriptparameters {parameters} --verbose".Split(' '));
var argService = new ArgumentsService(log, argParser);
var args = argService.GetArguments<ScriptArguments>();
- return args.ScriptParameters;
+ return args?.ScriptParameters;
}
[TestMethod]
- public void Illegal() => Assert.AreEqual(null, TestScript("hello nonsense"));
+ [ExpectedException(typeof(Exception))]
+ public void Illegal() => TestScript("hello nonsense");
[TestMethod]
public void SingleParam() => Assert.AreEqual("hello", TestScript("hello"));
diff --git a/src/main.test/Tests/OrderPluginTests/DomainPluginTests.cs b/src/main.test/Tests/OrderPluginTests/DomainPluginTests.cs
new file mode 100644
index 0000000..b002769
--- /dev/null
+++ b/src/main.test/Tests/OrderPluginTests/DomainPluginTests.cs
@@ -0,0 +1,24 @@
+using Autofac;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Plugins.OrderPlugins;
+using PKISharp.WACS.UnitTests.Mock;
+
+namespace PKISharp.WACS.UnitTests.Tests.EcnryptionTests
+{
+ [TestClass]
+ public class DomainPluginTests
+ {
+ [TestMethod]
+ public void DomainSplit()
+ {
+ var parts = new TargetPart[] { new TargetPart(new[] { "x.com" }) };
+ var target = new Target("x.com", "x.com", parts);
+ var renewal = new Renewal();
+ var container = new MockContainer().TestScope();
+ var domain = container.Resolve<Domain>();
+ var split = domain.Split(renewal, target);
+ Assert.IsNotNull(split);
+ }
+ }
+}
diff --git a/src/main.test/Tests/RenewalTests/RenewalServiceTests.cs b/src/main.test/Tests/RenewalTests/RenewalServiceTests.cs
index 9ab8c90..6c17610 100644
--- a/src/main.test/Tests/RenewalTests/RenewalServiceTests.cs
+++ b/src/main.test/Tests/RenewalTests/RenewalServiceTests.cs
@@ -10,6 +10,7 @@ using PKISharp.WACS.Clients;
using PKISharp.WACS.Clients.DNS;
using System.Linq;
using System.Collections.Generic;
+using PKISharp.WACS.UnitTests.Mock;
namespace PKISharp.WACS.UnitTests.Tests.RenewalTests
{
@@ -19,56 +20,13 @@ namespace PKISharp.WACS.UnitTests.Tests.RenewalTests
[TestMethod]
public void Simple()
{
- var log = new mock.LogService(false);
- var pluginService = new real.PluginService(log);
- var argumentsParser = new ArgumentsParser(log, pluginService, $"".Split(' '));
- var argumentsService = new real.ArgumentsService(log, argumentsParser);
- var input = new mock.InputService(new List<string>()
- {
- "C", // Cancel command
- "y", // Confirm cancel all
- "Q" // Quit
- });
-
- var builder = new ContainerBuilder();
- _ = builder.RegisterInstance(log).As<real.ILogService>();
- _ = builder.RegisterInstance(argumentsParser).As<ArgumentsParser>();
- _ = builder.RegisterInstance(argumentsService).As<real.IArgumentsService>();
- _ = builder.RegisterInstance(argumentsService).As<real.IArgumentsService>();
- _ = builder.RegisterInstance(pluginService).As<real.IPluginService>();
- _ = builder.RegisterInstance(input).As<real.IInputService>();
-
- _ = builder.RegisterType<mock.MockRenewalStore>().As<real.IRenewalStore>().SingleInstance();
- _ = builder.RegisterType<mock.MockSettingsService>().As<real.ISettingsService>().SingleInstance(); ;
- _ = builder.RegisterType<mock.UserRoleService>().As<real.IUserRoleService>().SingleInstance();
- _ = builder.RegisterType<real.ProxyService>().SingleInstance();
- _ = builder.RegisterType<real.PasswordGenerator>().SingleInstance();
-
- pluginService.Configure(builder);
-
- _ = builder.RegisterType<real.DomainParseService>().SingleInstance();
- _ = builder.RegisterType<Mock.Clients.MockIISClient>().As<IIISClient>().SingleInstance();
- _ = builder.RegisterType<IISHelper>().SingleInstance();
- _ = builder.RegisterType<real.ExceptionHandler>().SingleInstance();
- _ = builder.RegisterType<UnattendedResolver>();
- _ = builder.RegisterType<InteractiveResolver>();
- _ = builder.RegisterType<real.AutofacBuilder>().As<real.IAutofacBuilder>().SingleInstance();
- _ = builder.RegisterType<AcmeClient>().SingleInstance();
- _ = builder.RegisterType<real.PemService>().SingleInstance();
- _ = builder.RegisterType<EmailClient>().SingleInstance();
- _ = builder.RegisterType<ScriptClient>().SingleInstance();
- _ = builder.RegisterType<LookupClientProvider>().SingleInstance();
- _ = builder.RegisterType<mock.CertificateService>().As<real.ICertificateService>().SingleInstance();
- _ = builder.RegisterType<real.TaskSchedulerService>().SingleInstance();
- _ = builder.RegisterType<real.NotificationService>().SingleInstance();
- _ = builder.RegisterType<RenewalExecutor>().SingleInstance();
- _ = builder.RegisterType<RenewalManager>().SingleInstance();
- _ = builder.Register(c => c.Resolve<real.IArgumentsService>().MainArguments).SingleInstance();
-
- var container = builder.Build();
+ 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.test/wacs.test.csproj b/src/main.test/wacs.test.csproj
index 1cdbb54..35e2c74 100644
--- a/src/main.test/wacs.test.csproj
+++ b/src/main.test/wacs.test.csproj
@@ -15,10 +15,10 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
- <PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
- <PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
- <PackageReference Include="coverlet.collector" Version="1.2.0">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+ <PackageReference Include="MSTest.TestAdapter" Version="2.1.1" />
+ <PackageReference Include="MSTest.TestFramework" Version="2.1.1" />
+ <PackageReference Include="coverlet.collector" Version="1.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/src/main/Program.cs b/src/main/Program.cs
index 6237e56..e02a212 100644
--- a/src/main/Program.cs
+++ b/src/main/Program.cs
@@ -110,7 +110,7 @@ namespace PKISharp.WACS.Host
pluginService.Configure(builder);
_ = builder.RegisterType<DomainParseService>().SingleInstance();
- _ = builder.RegisterType<IISClient>().As<IIISClient>().SingleInstance();
+ _ = builder.RegisterType<IISClient>().As<IIISClient>().InstancePerLifetimeScope();
_ = builder.RegisterType<IISHelper>().SingleInstance();
_ = builder.RegisterType<ExceptionHandler>().SingleInstance();
_ = builder.RegisterType<UnattendedResolver>();
@@ -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 7657b14..de50241 100644
--- a/src/main/settings.json
+++ b/src/main/settings.json
@@ -59,8 +59,9 @@
"DefaultTarget": null
},
"Validation": {
- "DefaultValidation": "script",
- "DefaultValidationMode": "dns-01",
+ "DefaultValidation": null,
+ "DefaultValidationMode": null,
+ "DisableMultiThreading": false,
"CleanupFolders": true,
"PreValidateDns": true,
"PreValidateDnsRetryCount": 5,
@@ -76,10 +77,20 @@
},
"Store": {
"DefaultStore": null,
- "DefaultCertificateStore": null,
- "DefaultCentralSslStore": null,
- "DefaultCentralSslPfxPassword": null,
- "DefaultPemFilesPath": null
+ "CertificateStore": {
+ "DefaultStore": null
+ },
+ "CentralSsl": {
+ "DefaultPath": null,
+ "DefaultPassword": null
+ },
+ "PemFiles": {
+ "DefaultPath": null
+ },
+ "PfxFile": {
+ "DefaultPath": null,
+ "DefaultPassword": null
+ }
},
"Installation": {
"DefaultInstallation": null
diff --git a/src/plugin.validation.dns.azure/Azure.cs b/src/plugin.validation.dns.azure/Azure.cs
index ce9d2ab..b4d053e 100644
--- a/src/plugin.validation.dns.azure/Azure.cs
+++ b/src/plugin.validation.dns.azure/Azure.cs
@@ -4,33 +4,36 @@ 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 Uri _resourceManagerEndpoint;
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>>();
_resourceManagerEndpoint = new Uri(AzureEnvironments.ResourceManagerUrls[AzureEnvironments.AzureCloud]);
if (!string.IsNullOrEmpty(options.AzureEnvironment))
{
@@ -50,32 +53,117 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
}
}
- public override async Task 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);
- if (string.IsNullOrEmpty(zone))
+ var zone = await GetHostedZone(record.Authority.Domain);
+ if (zone == null)
{
- return;
+ 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;
+ }
- var subDomain = recordName.Substring(0, recordName.LastIndexOf(zone)).TrimEnd('.');
-
- // 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 = 3600,
- TxtRecords = new List<TxtRecord>
+ foreach (var domain in _recordSets[zone].Keys)
{
- new TxtRecord(new[] { token })
+ updateTasks.Add(CreateOrUpdateRecordSet(zone, domain));
}
- };
+ }
+ await Task.WhenAll(updateTasks);
+ }
- _ = await client.RecordSets.CreateOrUpdateAsync(_options.ResourceGroupName,
- zone,
- subDomain,
- RecordType.TXT,
- recordSetParams);
+ /// <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
+ {
+ 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 DNS records in {zone} ({domain})", zone, domain);
+ }
}
private async Task<DnsManagementClient> GetClient()
@@ -122,59 +210,65 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
};
}
- private async Task<string> GetHostedZone(string recordName)
+ /// <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 client = await GetClient();
- var domainName = _domainParser.GetDomain(recordName);
- 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, _options.ResourceGroupName);
+ var ret = recordName.Substring(0, recordName.LastIndexOf(zone)).TrimEnd('.');
+ return string.IsNullOrEmpty(ret) ? "@" : ret;
+ }
- var hostedZone = zones.Select(zone =>
+ /// <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)
+ {
+ // Cache so we don't have to repeat this more than once for each renewal
+ if (_hostedZones == null)
{
- var fit = 0;
- var name = zone.Name.TrimEnd('.').ToLowerInvariant();
- if (recordName.ToLowerInvariant().EndsWith(name))
+ 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))
{
- // If there is a zone for a.b.c.com (4) and one for c.com (2)
- // then the former is a better (more specific) match than the
- // latter, so we should use that
- fit = name.Split('.').Count();
+ response = await client.Zones.ListByResourceGroupNextAsync(response.NextPageLink);
}
- return new { zone, fit };
- }).
- Where(x => x.fit > 0).
- OrderByDescending(x => x.fit).
- FirstOrDefault();
+ _log.Debug("Found {count} hosted zones in Azure Resource Group {rg}", zones.Count, _options.ResourceGroupName);
+ _hostedZones = zones;
+ }
+ var hostedZone = FindBestMatch(_hostedZones.ToDictionary(x => x.Name), recordName);
if (hostedZone != null)
{
- return hostedZone.zone.Name;
+ return hostedZone.Name;
}
-
_log.Error(
- "Can't find hosted zone for {domainName} in resource group {ResourceGroupName}",
- domainName,
+ "Can't find hosted zone for {recordName} in resource group {ResourceGroupName}",
+ recordName,
_options.ResourceGroupName);
-
return null;
}
- public override async Task DeleteRecord(string recordName, string token)
- {
- var client = await GetClient();
- var zone = await GetHostedZone(recordName);
- var subDomain = recordName.Substring(0, recordName.LastIndexOf(zone)).TrimEnd('.');
- await client.RecordSets.DeleteAsync(
- _options.ResourceGroupName,
- zone,
- subDomain,
- RecordType.TXT);
- }
+ /// <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.azure/wacs.validation.dns.azure.csproj b/src/plugin.validation.dns.azure/wacs.validation.dns.azure.csproj
index efa9caa..b932057 100755
--- a/src/plugin.validation.dns.azure/wacs.validation.dns.azure.csproj
+++ b/src/plugin.validation.dns.azure/wacs.validation.dns.azure.csproj
@@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Management.Dns" Version="3.0.1" />
- <PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.4.0" />
+ <PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.5.0" />
<PackageReference Include="Microsoft.Rest.ClientRuntime.Azure.Authentication" Version="2.4.0" />
</ItemGroup>
diff --git a/src/plugin.validation.dns.cloudflare/Cloudflare.cs b/src/plugin.validation.dns.cloudflare/Cloudflare.cs
index 2937903..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,24 +24,21 @@ 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();
_domainParser = domainParser;
}
- private IAuthorizedSyntax GetContext()
- {
+ private IAuthorizedSyntax GetContext() =>
// avoid name collision with this class
- return FluentCloudflare.Cloudflare.WithToken(_options.ApiToken.Value);
- }
+ FluentCloudflare.Cloudflare.WithToken(_options.ApiToken.Value);
private async Task<Zone> GetHostedZone(IAuthorizedSyntax context, string recordName)
{
var prs = _domainParser;
- var domainName = $"{prs.GetDomain(recordName)}.{prs.GetTLD(recordName)}";
+ var domainName = $"{prs.GetRegisterableDomain(recordName)}";
var zonesResp = await context.Zones.List()
.WithName(domainName)
.ParseAsync(_hc)
@@ -48,33 +46,32 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
if (!zonesResp.Success || (zonesResp.Result?.Count ?? 0) < 1)
{
- _log.Error(
- "Zone {domainName} could not be found using the Cloudflare API. Maybe you entered a wrong API Token or domain or the API Token does not allow access to this domain?",
- domainName);
- // maybe throwing would be better
- // this is how the Azure DNS Validator works
- return null;
+ _log.Error("Zone {domainName} could not be found using the Cloudflare API." +
+ " Maybe you entered a wrong API Token or domain or the API Token does" +
+ " not allow access to this domain?", domainName);
+ throw new Exception();
}
-
return zonesResp.Unpack().First();
}
- public override async Task 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)
{
- throw new InvalidOperationException($"The zone could not be found using the Cloudflare API, thus creating a DNS validation record is impossible. " +
+ _log.Error("The zone could not be found using the Cloudflare API, thus creating a DNS validation record is impossible. " +
$"Please note you need to use an API Token, not the Global API Key. The token needs the permissions Zone.Zone:Read and Zone.DNS:Edit. Regarding " +
$"Zone:Read it is important, that this token has access to all zones in your account (Zone Resources > Include > All zones) because we need to " +
$"list your zones. Read the docs carefully for instructions.");
+ return false;
}
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;
}
private async Task DeleteRecord(string recordName, string token, IAuthorizedSyntax context, Zone zone)
@@ -90,17 +87,29 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
.ConfigureAwait(false);
var record = records.FirstOrDefault();
if (record == null)
- throw new Exception($"The record {recordName} that should be deleted does not exist at Cloudflare.");
- await dns.Delete(record.Id)
- .CallAsync(_hc)
- .ConfigureAwait(false);
+ {
+ _log.Warning($"The record {recordName} that should be deleted does not exist at Cloudflare.");
+ return;
+ }
+
+ try
+ {
+ _ = await dns.Delete(record.Id)
+ .CallAsync(_hc)
+ .ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _log.Warning($"Unable to delete record from Cloudflare: {ex.Message}");
+ }
+
}
- 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/DreamhostArgumentsProvider.cs b/src/plugin.validation.dns.dreamhost/DreamhostArgumentsProvider.cs
index 1449635..06a96fb 100644
--- a/src/plugin.validation.dns.dreamhost/DreamhostArgumentsProvider.cs
+++ b/src/plugin.validation.dns.dreamhost/DreamhostArgumentsProvider.cs
@@ -13,7 +13,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
public override void Configure(FluentCommandLineParser<DreamhostArguments> parser)
{
- parser.Setup(o => o.ApiKey)
+ _ = parser.Setup(o => o.ApiKey)
.As("apiKey")
.WithDescription("Dreamhost API key.");
}
diff --git a/src/plugin.validation.dns.dreamhost/DreamhostDnsValidation.cs b/src/plugin.validation.dns.dreamhost/DreamhostDnsValidation.cs
index 8c971b5..2638d63 100644
--- a/src/plugin.validation.dns.dreamhost/DreamhostDnsValidation.cs
+++ b/src/plugin.validation.dns.dreamhost/DreamhostDnsValidation.cs
@@ -1,6 +1,8 @@
using PKISharp.WACS.Clients.DNS;
+using PKISharp.WACS.Context;
using PKISharp.WACS.Plugins.ValidationPlugins.Dreamhost;
using PKISharp.WACS.Services;
+using System;
using System.Threading.Tasks;
namespace PKISharp.WACS.Plugins.ValidationPlugins
@@ -17,8 +19,29 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins
: base(dnsClient, logService, settings)
=> _client = new DnsManagementClient(options.ApiKey.Value, logService);
- public override Task CreateRecord(string recordName, string token) => _client.CreateRecord(recordName, RecordType.TXT, token);
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
+ {
+ try
+ {
+ await _client.CreateRecord(record.Authority.Domain, RecordType.TXT, record.Value);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
- public override Task DeleteRecord(string recordName, string token) => _client.DeleteRecord(recordName, RecordType.TXT, token);
+ public override async Task DeleteRecord(DnsValidationRecord record)
+ {
+ try
+ {
+ await _client.DeleteRecord(record.Authority.Domain, RecordType.TXT, record.Value);
+ }
+ catch (Exception ex)
+ {
+ _log.Warning($"Unable to delete record from Dreamhost: {ex.Message}");
+ }
+ }
}
}
diff --git a/src/plugin.validation.dns.luadns/luadns.cs b/src/plugin.validation.dns.luadns/luadns.cs
index 2cd561c..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;
@@ -12,7 +13,7 @@ using System.Threading.Tasks;
namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
{
- internal sealed class LUADNS : DnsValidation<LUADNS>
+ internal sealed class LuaDns : DnsValidation<LuaDns>
{
private class ZoneData
{
@@ -44,94 +45,83 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
public int TTL { get; set; }
}
- private static readonly Uri _luaDNSApiEndpoint = new Uri("https://api.luadns.com/v1/", UriKind.Absolute);
+ private static readonly Uri _LuaDnsApiEndpoint = new Uri("https://api.luadns.com/v1/", UriKind.Absolute);
private static readonly Dictionary<string, RecordData> _recordsMap = new Dictionary<string, RecordData>();
- private readonly DomainParseService _domainParser;
private readonly ProxyService _proxyService;
private readonly string _userName;
private readonly string _apiKey;
- public LUADNS(
+ public LuaDns(
LookupClientProvider dnsClient,
- DomainParseService domainParser,
ProxyService proxy,
ILogService log,
ISettingsService settings,
- LUADNSOptions options)
- : base(dnsClient, log, settings)
+ LuaDnsOptions options): base(dnsClient, log, settings)
{
- _domainParser = domainParser;
_proxyService = proxy;
-
_userName = options.Username;
_apiKey = options.APIKey.Value;
}
- public override async Task CreateRecord(string recordName, string token)
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
{
- _log.Information("Creating LUADNS verification record");
+ _log.Information("Creating LuaDNS verification record");
+
+ using var client = GetClient();
+ var response = await client.GetAsync(new Uri(_LuaDnsApiEndpoint, "zones"));
+ if (!response.IsSuccessStatusCode)
+ {
+ _log.Error("Failed to get DNS zones list for account. Aborting.");
+ return false;
+ }
- using (var client = GetClient())
+ var payload = await response.Content.ReadAsStringAsync();
+ var zones = JsonSerializer.Deserialize<ZoneData[]>(payload);
+ var targetZone = FindBestMatch(zones.ToDictionary(x => x.Name), record.Authority.Domain);
+ if (targetZone == null)
{
- var response = await client.GetAsync(new Uri(_luaDNSApiEndpoint, "zones"));
- if (!response.IsSuccessStatusCode)
- {
- _log.Information("Failed to get DNS zones list for account. Aborting.");
- return;
- }
-
- var payload = await response.Content.ReadAsStringAsync();
- var zones = JsonSerializer.Deserialize<ZoneData[]>(payload);
- var targetZone = zones.Where(d => recordName.EndsWith(d.Name, StringComparison.InvariantCultureIgnoreCase)).OrderByDescending(d => d.Name.Length).FirstOrDefault();
- if (targetZone == null)
- {
- _log.Information("No matching zone found in LUADNS account. Aborting");
- return;
- }
-
- var newRecord = new RecordData { Name = $"{recordName}.", Type = "TXT", Content = token, TTL = 300 };
- payload = JsonSerializer.Serialize(newRecord);
-
- response = await client.PostAsync(new Uri(_luaDNSApiEndpoint, $"zones/{targetZone.Id}/records"), new StringContent(payload, Encoding.UTF8, "application/json"));
- if (!response.IsSuccessStatusCode)
- {
- _log.Information("Failed to create DNS verification record");
- return;
- }
-
- payload = await response.Content.ReadAsStringAsync();
- newRecord = JsonSerializer.Deserialize<RecordData>(payload);
-
- _recordsMap[recordName] = newRecord;
-
- _log.Information("DNS Record created. Waiting 30 seconds to allow propagation.");
- await Task.Delay(30000);
+ _log.Error("No matching zone found in LuaDNS account. Aborting");
+ return false;
}
+
+ 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"));
+ if (!response.IsSuccessStatusCode)
+ {
+ _log.Error("Failed to create DNS verification record");
+ return false;
+ }
+
+ payload = await response.Content.ReadAsStringAsync();
+ newRecord = JsonSerializer.Deserialize<RecordData>(payload);
+ _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.Information($"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");
+ _log.Information("Deleting LuaDNS verification record");
- using (var client = GetClient())
+ using var client = GetClient();
+ var created = _recordsMap[record.Authority.Domain];
+ var response = await client.DeleteAsync(new Uri(_LuaDnsApiEndpoint, $"zones/{created.ZoneId}/records/{created.Id}"));
+ if (!response.IsSuccessStatusCode)
{
- var response = await client.DeleteAsync(new Uri(_luaDNSApiEndpoint, $"zones/{_recordsMap[recordName].ZoneId}/records/{_recordsMap[recordName].Id}"));
- if (!response.IsSuccessStatusCode)
- {
- _log.Information("Failed to delete DNS verification record");
- return;
- }
-
- _recordsMap.Remove(recordName);
+ _log.Warning("Failed to delete DNS verification record");
+ return;
}
+
+ _ = _recordsMap.Remove(record.Authority.Domain);
}
private HttpClient GetClient()
diff --git a/src/plugin.validation.dns.luadns/luadnsArguments.cs b/src/plugin.validation.dns.luadns/luadnsArguments.cs
index 13ab07d..cdb4dea 100644
--- a/src/plugin.validation.dns.luadns/luadnsArguments.cs
+++ b/src/plugin.validation.dns.luadns/luadnsArguments.cs
@@ -1,8 +1,8 @@
namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
{
- public sealed class LUADNSArguments
+ public sealed class LuaDnsArguments
{
- public string LUADNSUsername { get; set; }
- public string LUADNSAPIKey { get; set; }
+ public string LuaDnsUsername { get; set; }
+ public string LuaDnsAPIKey { get; set; }
}
} \ No newline at end of file
diff --git a/src/plugin.validation.dns.luadns/luadnsArgumentsProvider.cs b/src/plugin.validation.dns.luadns/luadnsArgumentsProvider.cs
index 9cdf9e6..5befdb3 100644
--- a/src/plugin.validation.dns.luadns/luadnsArgumentsProvider.cs
+++ b/src/plugin.validation.dns.luadns/luadnsArgumentsProvider.cs
@@ -3,20 +3,20 @@ using PKISharp.WACS.Configuration;
namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
{
- public sealed class LUADNSArgumentsProvider : BaseArgumentsProvider<LUADNSArguments>
+ public sealed class LuaDnsArgumentsProvider : BaseArgumentsProvider<LuaDnsArguments>
{
- public override string Name { get; } = "LUADNS";
+ public override string Name { get; } = "LuaDns";
public override string Group { get; } = "Validation";
- public override string Condition { get; } = "--validationmode dns-01 --validation luadns";
- public override void Configure(FluentCommandLineParser<LUADNSArguments> parser)
+ public override string Condition { get; } = "--validationmode dns-01 --validation LuaDns";
+ public override void Configure(FluentCommandLineParser<LuaDnsArguments> parser)
{
- parser.Setup(_ => _.LUADNSUsername)
- .As("LUADNSUsername")
- .WithDescription("LUADN account useername (email address)");
+ _ = parser.Setup(_ => _.LuaDnsUsername)
+ .As("LuaDnsUsername")
+ .WithDescription("LuaDNS account username (email address)");
- parser.Setup(_ => _.LUADNSAPIKey)
- .As("LUADNSAPIKey")
- .WithDescription("LUADNS API Key");
+ _ = parser.Setup(_ => _.LuaDnsAPIKey)
+ .As("LuaDnsAPIKey")
+ .WithDescription("LuaDNS API key");
}
}
} \ No newline at end of file
diff --git a/src/plugin.validation.dns.luadns/luadnsOptions.cs b/src/plugin.validation.dns.luadns/luadnsOptions.cs
index 348b4bd..1804774 100644
--- a/src/plugin.validation.dns.luadns/luadnsOptions.cs
+++ b/src/plugin.validation.dns.luadns/luadnsOptions.cs
@@ -6,10 +6,10 @@ using PKISharp.WACS.Services.Serialization;
namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
{
[Plugin("3b0c3cca-db98-40b7-b678-b34791070d42")]
- internal sealed class LUADNSOptions : ValidationPluginOptions<LUADNS>
+ internal sealed class LuaDnsOptions : ValidationPluginOptions<LuaDns>
{
- public override string Name { get; } = "LUADNS";
- public override string Description { get; } = "Create verification records in LUADNS";
+ public override string Name { get; } = "LuaDns";
+ public override string Description { get; } = "Create verification records in LuaDns";
public override string ChallengeType { get; } = Constants.Dns01ChallengeType;
public string Username { get; set; }
diff --git a/src/plugin.validation.dns.luadns/luadnsOptionsFactory.cs b/src/plugin.validation.dns.luadns/luadnsOptionsFactory.cs
index c6357cd..95d4384 100644
--- a/src/plugin.validation.dns.luadns/luadnsOptionsFactory.cs
+++ b/src/plugin.validation.dns.luadns/luadnsOptionsFactory.cs
@@ -7,29 +7,29 @@ using System.Threading.Tasks;
namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
{
- internal sealed class LUADNSOptionsFactory : ValidationPluginOptionsFactory<LUADNS, LUADNSOptions>
+ internal sealed class LuaDnsOptionsFactory : ValidationPluginOptionsFactory<LuaDns, LuaDnsOptions>
{
private readonly IArgumentsService _arguments;
- public LUADNSOptionsFactory(IArgumentsService arguments) : base(Dns01ChallengeValidationDetails.Dns01ChallengeType) => _arguments = arguments;
+ public LuaDnsOptionsFactory(IArgumentsService arguments) : base(Dns01ChallengeValidationDetails.Dns01ChallengeType) => _arguments = arguments;
- public override async Task<LUADNSOptions> Aquire(Target target, IInputService input, RunLevel runLevel)
+ public override async Task<LuaDnsOptions> Aquire(Target target, IInputService input, RunLevel runLevel)
{
- var args = _arguments.GetArguments<LUADNSArguments>();
- var opts = new LUADNSOptions
+ var args = _arguments.GetArguments<LuaDnsArguments>();
+ var opts = new LuaDnsOptions
{
- Username = await _arguments.TryGetArgument(args.LUADNSUsername, input, "LUADNS Account username"),
- APIKey = new ProtectedString(await _arguments.TryGetArgument(args.LUADNSAPIKey, input, "LUADNS API key", true))
+ Username = await _arguments.TryGetArgument(args.LuaDnsUsername, input, "LuaDns Account username"),
+ APIKey = new ProtectedString(await _arguments.TryGetArgument(args.LuaDnsAPIKey, input, "LuaDns API key", true))
};
return opts;
}
- public override Task<LUADNSOptions> Default(Target target)
+ public override Task<LuaDnsOptions> Default(Target target)
{
- var args = _arguments.GetArguments<LUADNSArguments>();
- var opts = new LUADNSOptions
+ var args = _arguments.GetArguments<LuaDnsArguments>();
+ var opts = new LuaDnsOptions
{
- Username = _arguments.TryGetRequiredArgument(nameof(args.LUADNSUsername), args.LUADNSUsername),
- APIKey = new ProtectedString(_arguments.TryGetRequiredArgument(nameof(args.LUADNSAPIKey), args.LUADNSAPIKey))
+ Username = _arguments.TryGetRequiredArgument(nameof(args.LuaDnsUsername), args.LuaDnsUsername),
+ APIKey = new ProtectedString(_arguments.TryGetRequiredArgument(nameof(args.LuaDnsAPIKey), args.LuaDnsAPIKey))
};
return Task.FromResult(opts);
}
diff --git a/src/plugin.validation.dns.luadns/wacs.validation.dns.luadns.csproj b/src/plugin.validation.dns.luadns/wacs.validation.dns.luadns.csproj
index 3749682..740a4c6 100644
--- a/src/plugin.validation.dns.luadns/wacs.validation.dns.luadns.csproj
+++ b/src/plugin.validation.dns.luadns/wacs.validation.dns.luadns.csproj
@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
- <AssemblyName>PKISharp.WACS.Plugins.ValidationPlugins.LUADNS</AssemblyName>
+ <AssemblyName>PKISharp.WACS.Plugins.ValidationPlugins.LuaDns</AssemblyName>
<RootNamespace>PKISharp.WACS.Plugins.ValidationPlugins</RootNamespace>
</PropertyGroup>
diff --git a/src/plugin.validation.dns.route53/Route53.cs b/src/plugin.validation.dns.route53/Route53.cs
index 168d734..38aae55 100644
--- a/src/plugin.validation.dns.route53/Route53.cs
+++ b/src/plugin.validation.dns.route53/Route53.cs
@@ -3,7 +3,9 @@ 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;
@@ -13,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 };
@@ -32,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)
@@ -47,84 +45,79 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
};
}
- public override async Task CreateRecord(string recordName, string token)
+ public override async Task<bool> CreateRecord(DnsValidationRecord record)
{
- var hostedZoneId = await GetHostedZoneId(recordName);
- if (hostedZoneId != null)
+ try
{
- _log.Information($"Creating TXT record {recordName} with value {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 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 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)
+ {
+ _log.Warning($"Error creating TXT record: {ex.Message}");
+ return false;
}
}
- public override async Task DeleteRecord(string recordName, string token)
+ public override async Task DeleteRecord(DnsValidationRecord record)
{
- var hostedZoneId = await GetHostedZoneId(recordName);
- if (hostedZoneId != null)
- {
- _log.Information($"Deleting TXT record {recordName} with value {token}");
- _ = await _route53Client.ChangeResourceRecordSetsAsync(
+ var recordName = record.Authority.Domain;
+ var token = record.Value;
+ var hostedZoneIds = await GetHostedZoneIds(recordName);
+ _log.Information($"Deleting TXT record {recordName} with value {token}");
+ var deleteTasks = hostedZoneIds.Select(hostedZoneId =>
+ _route53Client.ChangeResourceRecordSetsAsync(
new ChangeResourceRecordSetsRequest(hostedZoneId,
new ChangeBatch(new List<Change> {
- new Change(
- ChangeAction.DELETE,
- CreateResourceRecordSet(recordName, token))
- })));
- }
+ 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 domainName = _domainParser.GetDomain(recordName);
var hostedZones = new List<HostedZone>();
var response = await _route53Client.ListHostedZonesAsync();
hostedZones.AddRange(response.HostedZones);
while (response.IsTruncated)
{
response = await _route53Client.ListHostedZonesAsync(
- new ListHostedZonesRequest() {
- Marker = response.NextMarker
+ new ListHostedZonesRequest() {
+ Marker = response.NextMarker
});
hostedZones.AddRange(response.HostedZones);
}
_log.Debug("Found {count} hosted zones in AWS", hostedZones);
-
- var hostedZone = hostedZones.Select(zone =>
- {
- var fit = 0;
- var name = zone.Name.TrimEnd('.').ToLowerInvariant();
- if (recordName.ToLowerInvariant().EndsWith(name))
- {
- // If there is a zone for a.b.c.com (4) and one for c.com (2)
- // then the former is a better (more specific) match than the
- // latter, so we should use that
- fit = name.Split('.').Count();
- _log.Verbose("Zone {name} scored {fit} points", zone.Name, fit);
- }
- else
- {
- _log.Verbose("Zone {name} not matched", zone.Name);
- }
- return new { zone, fit };
- }).
- Where(x => x.fit > 0).
- OrderByDescending(x => x.fit).
- FirstOrDefault();
+ 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.zone.Id;
+ return hostedZone.Select(x => x.Id);
}
-
- _log.Error($"Can't find hosted zone for domain {domainName}");
+ _log.Error($"Can't find hosted zone for domain {recordName}");
return null;
}
@@ -141,7 +134,7 @@ namespace PKISharp.WACS.Plugins.ValidationPlugins.Dns
while ((await _route53Client.GetChangeAsync(changeRequest)).ChangeInfo.Status == ChangeStatus.PENDING)
{
- await Task.Delay(5000);
+ await Task.Delay(2000);
}
}
}
diff --git a/src/plugin.validation.dns.route53/wacs.validation.dns.route53.csproj b/src/plugin.validation.dns.route53/wacs.validation.dns.route53.csproj
index 0209e91..4640fb5 100644
--- a/src/plugin.validation.dns.route53/wacs.validation.dns.route53.csproj
+++ b/src/plugin.validation.dns.route53/wacs.validation.dns.route53.csproj
@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="AWSSDK.Route53" Version="3.3.102.98" />
+ <PackageReference Include="AWSSDK.Route53" Version="3.3.104.12" />
</ItemGroup>
<ItemGroup>