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
{
///
/// The OrderManager makes sure that we don't hit rate limits
///
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"));
}
///
/// Get a previously cached order or if its too old, create a new one
///
///
///
///
public async Task 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);
}
///
/// Update order details from the server
///
///
///
private async Task RefreshOrder(OrderDetails order)
{
_log.Debug("Refreshing order...");
var update = await _client.GetOrderDetails(order.OrderUrl);
order.Payload = update.Payload;
return order;
}
private async Task CreateOrder(IEnumerable identifiers, string cacheKey)
{
_log.Verbose("Creating order for hosts: {identifiers}", identifiers);
try
{
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;
}
catch (AcmeProtocolException ex)
{
_log.Error($"Failed to create order: {ex.ProblemDetail ?? ex.Message}");
}
catch (Exception ex)
{
_log.Error(ex, $"Failed to create order");
}
return null;
}
///
/// Check if we have a recent order that can be reused
/// to prevent hitting rate limits
///
///
///
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(content);
return order;
}
catch (Exception ex)
{
_log.Warning("Unable to read order cache: {ex}", ex.Message);
}
return null;
}
///
/// Delete files that are not valid anymore
///
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);
}
}
}
}
}
///
/// Test if a cached order file is still usable
///
///
private bool IsValid(FileInfo order) => order.LastWriteTime > DateTime.Now.AddDays(_settings.Cache.ReuseDays * -1);
///
/// Save order to disk
///
///
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);
}
}
}
}