(feature): implementing cache persistance

This commit is contained in:
Maksym Sadovnychyy 2024-06-01 23:48:26 +02:00
parent a661489b4f
commit 4359d317c0
6 changed files with 193 additions and 104 deletions

View File

@ -12,6 +12,13 @@ public class CertificateCache {
}
public class RegistrationCache {
/// <summary>
/// Field used to identify cache by account id
/// </summary>
public Guid AccountId { get; set; }
public Dictionary<string, CertificateCache>? CachedCerts { get; set; }
public byte[]? AccountKey { get; set; }
public string? Id { get; set; }

View File

@ -17,8 +17,8 @@ namespace MaksIT.LetsEncrypt.Services;
public interface ILetsEncryptService {
Task<IDomainResult> ConfigureClient(Guid sessionId, string url);
Task<IDomainResult> Init(Guid sessionId, string[] contacts, RegistrationCache? registrationCache);
RegistrationCache? GetRegistrationCache(Guid sessionId);
Task<IDomainResult> Init(Guid sessionId,Guid accountId, string[] contacts, RegistrationCache? registrationCache);
(RegistrationCache?, IDomainResult) GetRegistrationCache(Guid sessionId);
(string?, IDomainResult) GetTermsOfServiceUri(Guid sessionId);
Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(Guid sessionId, string[] hostnames, string challengeType);
Task<IDomainResult> CompleteChallenges(Guid sessionId);
@ -30,7 +30,7 @@ public interface ILetsEncryptService {
public class LetsEncryptService : ILetsEncryptService {
private readonly ILogger<LetsEncryptService> _logger;
private readonly HttpClient _httpClient;
private readonly IMemoryCache _cache;
private readonly IMemoryCache _memoryCache;
public LetsEncryptService(
ILogger<LetsEncryptService> logger,
@ -38,13 +38,13 @@ public class LetsEncryptService : ILetsEncryptService {
IMemoryCache cache) {
_logger = logger;
_httpClient = httpClient;
_cache = cache;
_memoryCache = cache;
}
private State GetOrCreateState(Guid sessionId) {
if (!_cache.TryGetValue(sessionId, out State state)) {
if (!_memoryCache.TryGetValue(sessionId, out State state)) {
state = new State();
_cache.Set(sessionId, state, TimeSpan.FromHours(1));
_memoryCache.Set(sessionId, state, TimeSpan.FromHours(1));
}
return state;
}
@ -74,7 +74,7 @@ public class LetsEncryptService : ILetsEncryptService {
#endregion
#region Init
public async Task<IDomainResult> Init(Guid sessionId, string[] contacts, RegistrationCache? cache) {
public async Task<IDomainResult> Init(Guid sessionId, Guid accountId, string[] contacts, RegistrationCache? cache) {
if (sessionId == Guid.Empty) {
_logger.LogError("Invalid sessionId");
return IDomainResult.Failed();
@ -119,6 +119,8 @@ public class LetsEncryptService : ILetsEncryptService {
}
state.Cache = new RegistrationCache {
AccountId = accountId,
Location = account.Result.Location,
AccountKey = accountKey.ExportCspBlob(true),
Id = account.Result.Id,
@ -135,12 +137,15 @@ public class LetsEncryptService : ILetsEncryptService {
}
}
#endregion
public RegistrationCache? GetRegistrationCache(Guid sessionId) {
public (RegistrationCache?, IDomainResult) GetRegistrationCache(Guid sessionId) {
var state = GetOrCreateState(sessionId);
return state.Cache;
if(state?.Cache == null)
return IDomainResult.Failed<RegistrationCache?>();
return IDomainResult.Success(state.Cache);
}
#region GetTermsOfService

View File

@ -0,0 +1,18 @@
namespace LetsEncryptServer.BackgroundServices {
public class AutoRenewal : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
// Your background task logic here
Console.WriteLine("Background service is running.");
// Simulate some work by delaying for 5 seconds
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
public override Task StopAsync(CancellationToken stoppingToken) {
Console.WriteLine("Background service is stopping.");
return base.StopAsync(stoppingToken);
}
}
}

View File

@ -27,6 +27,7 @@ builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<ILetsEncryptService, LetsEncryptService>();
builder.Services.AddScoped<ICertsFlowService, CertsFlowService>();
builder.Services.AddSingleton<ICacheService, CacheService>();
var app = builder.Build();

View File

@ -0,0 +1,123 @@
using System.Text.Json;
using DomainResults.Common;
using MaksIT.LetsEncrypt.Entities;
namespace MaksIT.LetsEncryptServer.Services;
public interface ICacheService {
Task<(RegistrationCache?, IDomainResult)> LoadFromCacheAsync(Guid accountId);
Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache);
Task<IDomainResult> DeleteFromCacheAsync(Guid accountId);
}
public class CacheService : ICacheService, IDisposable {
private readonly ILogger<CacheService> _logger;
private readonly string _cacheDirectory;
private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1, 1);
public CacheService(
ILogger<CacheService> logger
) {
_logger = logger;
_cacheDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "cache");
if (!Directory.Exists(_cacheDirectory)) {
Directory.CreateDirectory(_cacheDirectory);
}
}
private string GetCacheFilePath(Guid accountId) {
return Path.Combine(_cacheDirectory, $"{accountId}.json");
}
public async Task<(RegistrationCache?, IDomainResult)> LoadFromCacheAsync(Guid accountId) {
var cacheFilePath = GetCacheFilePath(accountId);
await _cacheLock.WaitAsync();
try {
if (!File.Exists(cacheFilePath)) {
var message = $"Cache file not found for account {accountId}";
_logger.LogWarning(message);
return IDomainResult.Failed<RegistrationCache>(message);
}
var json = await File.ReadAllTextAsync(cacheFilePath);
if (string.IsNullOrEmpty(json)) {
var message = $"Cache file is empty for account {accountId}";
_logger.LogWarning(message);
return IDomainResult.Failed<RegistrationCache>(message);
}
var cache = JsonSerializer.Deserialize<RegistrationCache>(json);
return IDomainResult.Success(cache);
}
catch (Exception ex) {
var message = "Error reading cache file for account {accountId}";
_logger.LogError(ex, message);
return IDomainResult.Failed<RegistrationCache?>(message);
}
finally {
_cacheLock.Release();
}
}
public async Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache) {
var cacheFilePath = GetCacheFilePath(accountId);
await _cacheLock.WaitAsync();
try {
var json = JsonSerializer.Serialize(cache);
await File.WriteAllTextAsync(cacheFilePath, json);
_logger.LogInformation($"Cache file saved for account {accountId}");
return DomainResult.Success();
}
catch (Exception ex) {
var message = "Error writing cache file for account {accountId}";
_logger.LogError(ex, message);
return IDomainResult.Failed(message);
}
finally {
_cacheLock.Release();
}
}
public async Task<IDomainResult> DeleteFromCacheAsync(Guid accountId) {
var cacheFilePath = GetCacheFilePath(accountId);
await _cacheLock.WaitAsync();
try {
if (File.Exists(cacheFilePath)) {
File.Delete(cacheFilePath);
_logger.LogInformation($"Cache file deleted for account {accountId}");
}
else {
_logger.LogWarning($"Cache file not found for account {accountId}");
}
return IDomainResult.Success();
}
catch (Exception ex) {
var message = $"Error deleting cache file for account {accountId}";
_logger.LogError(ex, message);
return IDomainResult.Failed(message);
}
finally {
_cacheLock.Release();
}
}
public void Dispose() {
_cacheLock?.Dispose();
}
}

View File

@ -32,17 +32,22 @@ public class CertsFlowService : ICertsFlowService {
private readonly Configuration _appSettings;
private readonly ILogger<CertsFlowService> _logger;
private readonly ILetsEncryptService _letsEncryptService;
private readonly string _acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme");
private readonly ICacheService _cacheService;
private readonly string _acmePath;
public CertsFlowService(
IOptions<Configuration> appSettings,
ILogger<CertsFlowService> logger,
ILetsEncryptService letsEncryptService
ILetsEncryptService letsEncryptService,
ICacheService cashService
) {
_appSettings = appSettings.Value;
_logger = logger;
_letsEncryptService = letsEncryptService;
_cacheService = cashService;
_acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme");
if (!Directory.Exists(_acmePath))
Directory.CreateDirectory(_acmePath);
}
@ -70,12 +75,22 @@ public class CertsFlowService : ICertsFlowService {
}
public async Task<(Guid?, IDomainResult)> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData) {
var cache = default(RegistrationCache);
RegistrationCache? cache = null;
if (accountId == null) {
accountId = Guid.NewGuid();
}
else {
var (loadedCache, loadCaceResutl) = await _cacheService.LoadFromCacheAsync(accountId.Value);
if (!loadCaceResutl.IsSuccess || loadCaceResutl == null) {
accountId = Guid.NewGuid();
}
else {
cache = loadedCache;
}
}
var result = await _letsEncryptService.Init(sessionId, requestData.Contacts, cache);
var result = await _letsEncryptService.Init(sessionId, accountId.Value, requestData.Contacts, cache);
return result.IsSuccess ? IDomainResult.Success<Guid>(accountId.Value) : (null, result);
}
@ -111,6 +126,15 @@ public class CertsFlowService : ICertsFlowService {
Thread.Sleep(1000);
}
// Persist the cache
var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId);
if (!getCacheResult.IsSuccess || cache == null)
return getCacheResult;
var saveResult = await _cacheService.SaveToCacheAsync(cache.AccountId, cache);
if(!saveResult.IsSuccess)
return saveResult;
return IDomainResult.Success();
}
@ -178,98 +202,9 @@ public class CertsFlowService : ICertsFlowService {
return IDomainResult.Success();
}
/**
abort ssl cert <certfile> : abort a transaction for a certificate file
add acl [@<ver>] <acl> <pattern> : add an acl entry
add map [@<ver>] <map> <key> <val> : add a map entry (payload supported instead of key/val)
add ssl crt-list <list> <cert> [opts]* : add to crt-list file <list> a line <cert> or a payload
clear acl [@<ver>] <acl> : clear the contents of this acl
clear counters [all] : clear max statistics counters (or all counters)
clear map [@<ver>] <map> : clear the contents of this map
clear table <table> [<filter>]* : remove an entry from a table (filter: data/key)
commit acl @<ver> <acl> : commit the ACL at this version
commit map @<ver> <map> : commit the map at this version
commit ssl cert <certfile> : commit a certificate file
del acl <acl> [<key>|#<ref>] : delete acl entries matching <key>
del map <map> [<key>|#<ref>] : delete map entries matching <key>
del ssl cert <certfile> : delete an unused certificate file
del ssl crt-list <list> <cert[:line]> : delete a line <cert> from crt-list file <list>
disable agent : disable agent checks
disable dynamic-cookie backend <bk> : disable dynamic cookies on a specific backend
disable frontend <frontend> : temporarily disable specific frontend
disable health : disable health checks
disable server (DEPRECATED) : disable a server for maintenance (use 'set server' instead)
enable agent : enable agent checks
enable dynamic-cookie backend <bk> : enable dynamic cookies on a specific backend
enable frontend <frontend> : re-enable specific frontend
enable health : enable health checks
enable server (DEPRECATED) : enable a disabled server (use 'set server' instead)
get acl <acl> <value> : report the patterns matching a sample for an ACL
get map <acl> <value> : report the keys and values matching a sample for a map
get var <name> : retrieve contents of a process-wide variable
get weight <bk>/<srv> : report a server's current weight
new ssl cert <certfile> : create a new certificate file to be used in a crt-list or a directory
operator : lower the level of the current CLI session to operator
prepare acl <acl> : prepare a new version for atomic ACL replacement
prepare map <acl> : prepare a new version for atomic map replacement
set dynamic-cookie-key backend <bk> <k> : change a backend secret key for dynamic cookies
set map <map> [<key>|#<ref>] <value> : modify a map entry
set maxconn frontend <frontend> <value> : change a frontend's maxconn setting
set maxconn global <value> : change the per-process maxconn setting
set maxconn server <bk>/<srv> : change a server's maxconn setting
set profiling <what> {auto|on|off} : enable/disable resource profiling (tasks,memory)
set rate-limit <setting> <value> : change a rate limiting value
set server <bk>/<srv> [opts] : change a server's state, weight, address or ssl
set severity-output [none|number|string]: set presence of severity level in feedback information
set ssl cert <certfile> <payload> : replace a certificate file
set ssl ocsp-response <resp|payload> : update a certificate's OCSP Response from a base64-encode DER
set ssl tls-key [id|file] <key> : set the next TLS key for the <id> or <file> listener to <key>
set table <table> key <k> [data.* <v>]* : update or create a table entry's data
set timeout [cli] <delay> : change a timeout setting
set weight <bk>/<srv> (DEPRECATED) : change a server's weight (use 'set server' instead)
show acl [@<ver>] <acl>] : report available acls or dump an acl's contents
show activity : show per-thread activity stats (for support/developers)
show backend : list backends in the current running config
show cache : show cache status
show cli level : display the level of the current CLI session
show cli sockets : dump list of cli sockets
show env [var] : dump environment variables known to the process
show errors [<px>] [request|response] : report last request and/or response errors for each proxy
show events [<sink>] [-w] [-n] : show event sink state
show fd [num] : dump list of file descriptors in use or a specific one
show info [desc|json|typed|float]* : report information about the running process
show libs : show loaded object files and libraries
show map [@ver] [map] : report available maps or dump a map's contents
show peers [dict|-] [section] : dump some information about all the peers or this peers section
show pools : report information about the memory pools usage
show profiling [<what>|<#lines>|byaddr]*: show profiling state (all,status,tasks,memory)
show resolvers [id] : dumps counters from all resolvers section and associated name servers
show schema json : report schema used for stats
show servers conn [<backend>] : dump server connections status (all or for a single backend)
show servers state [<backend>] : dump volatile server information (all or for a single backend)
show sess [id] : report the list of current sessions or dump this exact session
show ssl cert [<certfile>] : display the SSL certificates used in memory, or the details of a file
show ssl crt-list [-n] [<list>] : show the list of crt-lists or the content of a crt-list file <list>
show startup-logs : report logs emitted during HAProxy startup
show stat [desc|json|no-maint|typed|up]*: report counters for each proxy and server
show table <table> [<filter>]* : report table usage stats or dump this table's contents (filter: data/key)
show tasks : show running tasks
show threads : show some threads debugging information
show tls-keys [id|*] : show tls keys references or dump tls ticket keys when id specified
show trace [<module>] : show live tracing state
show version : show version of the current process
shutdown frontend <frontend> : stop a specific frontend
shutdown session [id] : kill a specific session
shutdown sessions server <bk>/<srv> : kill sessions on a server
trace [<module>|0] [cmd [args...]] : manage live tracing (empty to list, 0 to stop all)
user : lower the level of the current CLI session to user
help [<command>] : list matching or all commands
prompt : toggle interactive mode with prompt
quit : disconnect
*/
private IDomainResult NotifyHaproxy(IEnumerable<string> certFiles) {
var server = _appSettings.Server;
try {
using (var client = new TcpClient(server.Ip, server.SocketPort))
using (var networkStream = client.GetStream())