using Autofac;
using PKISharp.WACS.Configuration;
using PKISharp.WACS.DomainObjects;
using PKISharp.WACS.Extensions;
using PKISharp.WACS.Plugins.TargetPlugins;
using PKISharp.WACS.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace PKISharp.WACS
{
internal class RenewalManager
{
private readonly IInputService _input;
private readonly ILogService _log;
private readonly IRenewalStore _renewalStore;
private readonly IArgumentsService _arguments;
private readonly MainArguments _args;
private readonly IContainer _container;
private readonly IAutofacBuilder _scopeBuilder;
private readonly ExceptionHandler _exceptionHandler;
private readonly RenewalExecutor _renewalExecutor;
private readonly ISettingsService _settings;
public RenewalManager(
IArgumentsService arguments, MainArguments args,
IRenewalStore renewalStore, IContainer container,
IInputService input, ILogService log,
ISettingsService settings,
IAutofacBuilder autofacBuilder, ExceptionHandler exceptionHandler,
RenewalExecutor renewalExecutor)
{
_renewalStore = renewalStore;
_args = args;
_input = input;
_log = log;
_settings = settings;
_arguments = arguments;
_container = container;
_scopeBuilder = autofacBuilder;
_exceptionHandler = exceptionHandler;
_renewalExecutor = renewalExecutor;
}
///
/// Renewal management mode
///
///
internal async Task ManageRenewals()
{
IEnumerable originalSelection = _renewalStore.Renewals.OrderBy(x => x.LastFriendlyName);
var selectedRenewals = originalSelection;
var quit = false;
var displayAll = false;
do
{
var all = selectedRenewals.Count() == originalSelection.Count();
var none = selectedRenewals.Count() == 0;
var totalLabel = originalSelection.Count() != 1 ? "renewals" : "renewal";
var renewalSelectedLabel = selectedRenewals.Count() != 1 ? "renewals" : "renewal";
var selectionLabel =
all ? selectedRenewals.Count() == 1 ? "the renewal" : "*all* renewals" :
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.");
var displayRenewals = selectedRenewals;
var displayLimited = !displayAll && selectedRenewals.Count() >= _settings.UI.PageSize;
var displayHidden = 0;
var displayHiddenLabel = "";
if (displayLimited)
{
displayRenewals = displayRenewals.Take(_settings.UI.PageSize - 1);
displayHidden = selectedRenewals.Count() - displayRenewals.Count();
displayHiddenLabel = displayHidden != 1 ? "renewals" : "renewal";
}
var choices = displayRenewals.Select(x => Choice.Create(x,
description: x.ToString(_input),
color: x.History.LastOrDefault()?.Success ?? false ?
x.IsDue() ?
ConsoleColor.DarkYellow :
ConsoleColor.Green :
ConsoleColor.Red)).ToList();
if (displayLimited)
{
choices.Add(Choice.Create(null,
command: "More",
description: $"{displayHidden} additional {displayHiddenLabel} selected but currently not displayed"));
}
await _input.WritePagedList(choices);
displayAll = false;
var options = new List>>();
if (displayLimited)
{
options.Add(
Choice.Create>(
() => { displayAll = true; return Task.CompletedTask; },
"List all selected renewals", "A"));
}
if (selectedRenewals.Count() > 1)
{
options.Add(
Choice.Create>(
async () => selectedRenewals = await FilterRenewalsMenu(selectedRenewals),
all ? "Apply filter" : "Apply additional filter", "F",
@disabled: (selectedRenewals.Count() < 2, "Not enough renewals to filter.")));
options.Add(
Choice.Create>(
async () => selectedRenewals = await SortRenewalsMenu(selectedRenewals),
"Sort renewals", "S",
@disabled: (selectedRenewals.Count() < 2, "Not enough renewals to sort.")));
}
if (!all)
{
options.Add(
Choice.Create>(
() => { selectedRenewals = originalSelection; return Task.CompletedTask; },
"Reset sorting and filtering", "X",
@disabled: (all, "No filters have been applied yet.")));
}
options.Add(
Choice.Create>(
async () => {
foreach (var renewal in selectedRenewals) {
var index = selectedRenewals.ToList().IndexOf(renewal) + 1;
_log.Information("Details for renewal {n}/{m}", index, selectedRenewals.Count());
await ShowRenewal(renewal);
var cont = false;
if (index != selectedRenewals.Count())
{
cont = await _input.Wait("Press to continue or to abort");
if (!cont)
{
break;
}
}
else
{
await _input.Wait();
}
}
},
$"Show details for {selectionLabel}", "D",
@disabled: (none, "No renewals selected.")));
options.Add(
Choice.Create>(
async () => {
WarnAboutRenewalArguments();
foreach (var renewal in selectedRenewals)
{
var runLevel = RunLevel.Interactive | RunLevel.ForceRenew;
if (_args.Force)
{
runLevel |= RunLevel.IgnoreCache;
}
await ProcessRenewal(renewal, runLevel);
}
},
$"Run {selectionLabel}", "R",
@disabled: (none, "No renewals selected.")));
options.Add(
Choice.Create>(
async () => selectedRenewals = await Analyze(selectedRenewals),
$"Analyze duplicates for {selectionLabel}", "U",
@disabled: (none, "No renewals selected.")));
options.Add(
Choice.Create>(
async () => {
var confirm = await _input.PromptYesNo($"Are you sure you want to cancel {selectedRenewals.Count()} currently selected {renewalSelectedLabel}?", false);
if (confirm)
{
foreach (var renewal in selectedRenewals)
{
_renewalStore.Cancel(renewal);
};
originalSelection = _renewalStore.Renewals.OrderBy(x => x.LastFriendlyName);
selectedRenewals = originalSelection;
}
},
$"Cancel {selectionLabel}", "C",
@disabled: (none, "No renewals selected.")));
options.Add(
Choice.Create>(
async () => {
var confirm = await _input.PromptYesNo($"Are you sure you want to revoke the most recently issued certificate for {selectedRenewals.Count()} currently selected {renewalSelectedLabel}? This should only be done in case of a (suspected) security breach. Cancel the {renewalSelectedLabel} if you simply don't need the certificates anymore.", false);
if (confirm)
{
await RevokeCertificates(selectedRenewals);
}
},
$"Revoke certificate(s) for {selectionLabel}", "V",
@disabled: (none, "No renewals selected.")));
options.Add(
Choice.Create>(
() => { quit = true; return Task.CompletedTask; },
"Back", "Q",
@default: originalSelection.Count() == 0));
if (selectedRenewals.Count() > 1)
{
_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",
options,
(string unexpected) =>
Choice.Create>(
async () => selectedRenewals = await FilterRenewalsById(selectedRenewals, unexpected)));
await chosen.Invoke();
}
while (!quit);
}
///
/// Check if there are multiple renewals installing to the same site
/// or requesting certificates for the same domains
///
///
///
private async Task> Analyze(IEnumerable selectedRenewals)
{
var foundHosts = new Dictionary>();
var foundSites = new Dictionary>();
foreach (var renewal in selectedRenewals)
{
using var targetScope = _scopeBuilder.Target(_container, renewal, RunLevel.Unattended);
var target = targetScope.Resolve();
foreach (var targetPart in target.Parts)
{
if (targetPart.SiteId != null)
{
var siteId = targetPart.SiteId.Value;
if (!foundSites.ContainsKey(siteId))
{
foundSites.Add(siteId, new List());
}
foundSites[siteId].Add(renewal);
}
foreach (var host in targetPart.GetHosts(true))
{
if (!foundHosts.ContainsKey(host))
{
foundHosts.Add(host, new List());
}
foundHosts[host].Add(renewal);
}
}
}
// List results
var options = new List>>();
foreach (var site in foundSites)
{
if (site.Value.Count() > 1)
{
options.Add(
Choice.Create(
site.Value,
$"Select {site.Value.Count()} renewals covering IIS site {site.Key}"));
}
}
foreach (var host in foundHosts)
{
if (host.Value.Count() > 1)
{
options.Add(
Choice.Create(
host.Value,
$"Select {host.Value.Count()} renewals covering host {host.Key}"));
}
}
_input.CreateSpace();
if (options.Count == 0)
{
_input.Show(null, "Analysis didn't find any overlap between renewals.");
return selectedRenewals;
}
else
{
options.Add(
Choice.Create(
selectedRenewals.ToList(),
$"Back"));
_input.Show(null, "Analysis found some overlap between renewals. You can select the overlapping renewals from the menu.");
return await _input.ChooseFromMenu("Please choose from the menu", options);
}
}
///
/// Offer user different ways to sort the renewals
///
///
///
private async Task> SortRenewalsMenu(IEnumerable current)
{
var options = new List>>>
{
Choice.Create>>(
() => current.OrderBy(x => x.LastFriendlyName),
"Sort by friendly name",
@default: true),
Choice.Create>>(
() => current.OrderByDescending(x => x.LastFriendlyName),
"Sort by friendly name (descending)"),
Choice.Create>>(
() => current.OrderBy(x => x.GetDueDate()),
"Sort by due date"),
Choice.Create>>(
() => current.OrderByDescending(x => x.GetDueDate()),
"Sort by due date (descending)")
};
var chosen = await _input.ChooseFromMenu("How would you like to sort the renewals list?", options);
return chosen.Invoke();
}
///
/// Offer user different ways to filter the renewals
///
///
///
private async Task> FilterRenewalsMenu(IEnumerable current)
{
var options = new List>>>>
{
Choice.Create>>>(
() => FilterRenewalsByFriendlyName(current),
"Filter by friendly name"),
Choice.Create>>>(
() => Task.FromResult(current.Where(x => x.IsDue())),
"Keep only due renewals"),
Choice.Create>>>(
() => Task.FromResult(current.Where(x => !x.IsDue())),
"Remove due renewals"),
Choice.Create>>>(
() => Task.FromResult(current.Where(x => !x.History.Last().Success)),
"Keep only renewals with errors"),
Choice.Create>>>(
() => Task.FromResult(current.Where(x => x.History.Last().Success)),
"Remove renewals with errors"),
Choice.Create>>>(
() => Task.FromResult(current),
"Cancel")
};
var chosen = await _input.ChooseFromMenu("How would you like to filter?", options);
return await chosen.Invoke();
}
private async Task> FilterRenewalsById(IEnumerable current, string input)
{
var parts = input.ParseCsv();
if (parts == null)
{
return current;
}
var ret = new List();
foreach (var part in parts)
{
if (int.TryParse(part, out var index))
{
if (index > 0 && index <= current.Count())
{
ret.Add(current.ElementAt(index - 1));
}
else
{
_log.Warning("Input out of range: {part}", part);
}
}
else
{
_log.Warning("Invalid input: {part}", part);
}
}
return ret;
}
///
/// Filter specific renewals by friendly name
///
///
///
private async Task> FilterRenewalsByFriendlyName(IEnumerable current)
{
_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();
var regex = new Regex(rawInput.PatternToRegex(), RegexOptions.IgnoreCase);
foreach (var r in current)
{
if (regex.Match(r.LastFriendlyName).Success)
{
ret.Add(r);
}
}
return ret;
}
///
/// Filters for unattended mode
///
///
///
private async Task> FilterRenewalsByCommandLine(string command)
{
if (_arguments.HasFilter())
{
var targets = _renewalStore.FindByArguments(
_arguments.MainArguments.Id,
_arguments.MainArguments.FriendlyName);
if (targets.Count() == 0)
{
_log.Error("No renewals matched.");
}
return targets;
}
else
{
_log.Error($"Specify which renewal to {command} using the parameter --id or --friendlyname.");
}
return new List();
}
///
/// Loop through the store renewals and run those which are
/// due to be run
///
internal async Task CheckRenewals(RunLevel runLevel)
{
IEnumerable renewals;
if (_arguments.HasFilter())
{
renewals = _renewalStore.FindByArguments(_args.Id, _args.FriendlyName);
if (renewals.Count() == 0)
{
_log.Error("No renewals found that match the filter parameters --id and/or --friendlyname.");
}
}
else
{
_log.Verbose("Checking renewals");
renewals = _renewalStore.Renewals;
if (renewals.Count() == 0)
{
_log.Warning("No scheduled renewals found.");
}
}
if (renewals.Count() > 0)
{
WarnAboutRenewalArguments();
foreach (var renewal in renewals)
{
try
{
var success = await ProcessRenewal(renewal, runLevel);
if (!success)
{
// Make sure the ExitCode is set
_exceptionHandler.HandleException();
}
}
catch (Exception ex)
{
_exceptionHandler.HandleException(ex, "Unhandled error processing renewal");
continue;
}
}
}
}
///
/// Process a single renewal
///
///
internal async Task ProcessRenewal(Renewal renewal, RunLevel runLevel)
{
var notification = _container.Resolve();
try
{
var result = await _renewalExecutor.HandleRenewal(renewal, runLevel);
if (!result.Abort)
{
_renewalStore.Save(renewal, result);
if (result.Success)
{
await notification.NotifySuccess(renewal, _log.Lines);
return true;
}
else
{
await notification.NotifyFailure(runLevel, renewal, result.ErrorMessages, _log.Lines);
}
}
else
{
return true;
}
}
catch (Exception ex)
{
_exceptionHandler.HandleException(ex);
await notification.NotifyFailure(runLevel, renewal, new List { ex.Message }, _log.Lines);
}
return false;
}
///
/// Show a warning when the user appears to be trying to
/// use command line arguments in combination with a renew
/// command.
///
internal void WarnAboutRenewalArguments()
{
if (_arguments.Active)
{
_log.Warning("You have specified command line options for plugins. " +
"Note that these only affect new certificates, but NOT existing renewals. " +
"To change settings, re-create (overwrite) the renewal.");
}
}
///
/// Show certificate details
///
private async Task ShowRenewal(Renewal renewal)
{
try
{
_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);
_input.Show("Renewal due", renewal.GetDueDate()?.ToString() ?? "now");
_input.Show("Renewed", $"{renewal.History.Where(x => x.Success).Count()} times");
renewal.TargetPluginOptions.Show(_input);
renewal.ValidationPluginOptions.Show(_input);
if (renewal.OrderPluginOptions != null)
{
renewal.OrderPluginOptions.Show(_input);
}
if (renewal.CsrPluginOptions != null)
{
renewal.CsrPluginOptions.Show(_input);
}
foreach (var ipo in renewal.StorePluginOptions)
{
ipo.Show(_input);
}
foreach (var ipo in renewal.InstallationPluginOptions)
{
ipo.Show(_input);
}
var historyLimit = 10;
if (renewal.History.Count <= historyLimit)
{
_input.Show("History");
}
else
{
_input.Show($"History (most recent {historyLimit} of {renewal.History.Count} entries)");
}
await _input.WritePagedList(
renewal.History.
AsEnumerable().
Reverse().
Take(historyLimit).
Reverse().
Select(x => Choice.Create(x)));
}
catch (Exception ex)
{
_log.Error(ex, "Unable to list details for target");
}
}
#region Unattended
///
/// For command line --list
///
///
internal async Task ShowRenewalsUnattended()
{
await _input.WritePagedList(
_renewalStore.Renewals.Select(x => Choice.Create(x,
description: x.ToString(_input),
color: x.History.Last().Success ?
x.IsDue() ?
ConsoleColor.DarkYellow :
ConsoleColor.Green :
ConsoleColor.Red)));
}
///
/// Cancel certificate from the command line
///
///
internal async Task CancelRenewalsUnattended()
{
var targets = await FilterRenewalsByCommandLine("cancel");
foreach (var t in targets)
{
_renewalStore.Cancel(t);
}
}
///
/// Revoke certifcate from the command line
///
///
internal async Task RevokeCertificatesUnattended()
{
_log.Warning($"Certificates should only be revoked in case of a (suspected) security breach. Cancel the renewal if you simply don't need the certificate anymore.");
var renewals = await FilterRenewalsByCommandLine("revoke");
await RevokeCertificates(renewals);
}
///
/// Shared code for command line and renewal manager
///
///
///
internal async Task RevokeCertificates(IEnumerable renewals)
{
foreach (var renewal in renewals)
{
using var scope = _scopeBuilder.Execution(_container, renewal, RunLevel.Unattended);
var cs = scope.Resolve();
try
{
await cs.RevokeCertificate(renewal);
renewal.History.Add(new RenewResult("Certificate(s) revoked"));
}
catch (Exception ex)
{
_exceptionHandler.HandleException(ex);
}
}
}
#endregion
}
}