diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs b/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs index 6aee7b4..0c49e77 100644 --- a/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs +++ b/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs @@ -12,6 +12,13 @@ public class CertificateCache { } public class RegistrationCache { + + /// + /// Field used to identify cache by account id + /// + public Guid AccountId { get; set; } + + public Dictionary? CachedCerts { get; set; } public byte[]? AccountKey { get; set; } public string? Id { get; set; } diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs index a09f6ed..4aed135 100644 --- a/src/LetsEncrypt/Services/LetsEncryptService.cs +++ b/src/LetsEncrypt/Services/LetsEncryptService.cs @@ -17,8 +17,8 @@ namespace MaksIT.LetsEncrypt.Services; public interface ILetsEncryptService { Task ConfigureClient(Guid sessionId, string url); - Task Init(Guid sessionId, string[] contacts, RegistrationCache? registrationCache); - RegistrationCache? GetRegistrationCache(Guid sessionId); + Task Init(Guid sessionId,Guid accountId, string[] contacts, RegistrationCache? registrationCache); + (RegistrationCache?, IDomainResult) GetRegistrationCache(Guid sessionId); (string?, IDomainResult) GetTermsOfServiceUri(Guid sessionId); Task<(Dictionary?, IDomainResult)> NewOrder(Guid sessionId, string[] hostnames, string challengeType); Task CompleteChallenges(Guid sessionId); @@ -30,7 +30,7 @@ public interface ILetsEncryptService { public class LetsEncryptService : ILetsEncryptService { private readonly ILogger _logger; private readonly HttpClient _httpClient; - private readonly IMemoryCache _cache; + private readonly IMemoryCache _memoryCache; public LetsEncryptService( ILogger 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 Init(Guid sessionId, string[] contacts, RegistrationCache? cache) { + public async Task 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(); + + return IDomainResult.Success(state.Cache); } #region GetTermsOfService diff --git a/src/LetsEncryptServer/BackgroundServices/AutoRenewal.cs b/src/LetsEncryptServer/BackgroundServices/AutoRenewal.cs new file mode 100644 index 0000000..5a14e10 --- /dev/null +++ b/src/LetsEncryptServer/BackgroundServices/AutoRenewal.cs @@ -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); + } + } +} diff --git a/src/LetsEncryptServer/Program.cs b/src/LetsEncryptServer/Program.cs index 3c87b9a..756ac93 100644 --- a/src/LetsEncryptServer/Program.cs +++ b/src/LetsEncryptServer/Program.cs @@ -27,6 +27,7 @@ builder.Services.AddMemoryCache(); builder.Services.AddHttpClient(); builder.Services.AddScoped(); +builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/src/LetsEncryptServer/Services/CacheService.cs b/src/LetsEncryptServer/Services/CacheService.cs new file mode 100644 index 0000000..e9f3de8 --- /dev/null +++ b/src/LetsEncryptServer/Services/CacheService.cs @@ -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 SaveToCacheAsync(Guid accountId, RegistrationCache cache); + Task DeleteFromCacheAsync(Guid accountId); +} + +public class CacheService : ICacheService, IDisposable { + + private readonly ILogger _logger; + private readonly string _cacheDirectory; + private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1, 1); + + public CacheService( + ILogger 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(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(message); + } + + var cache = JsonSerializer.Deserialize(json); + return IDomainResult.Success(cache); + } + catch (Exception ex) { + var message = "Error reading cache file for account {accountId}"; + _logger.LogError(ex, message); + + return IDomainResult.Failed(message); + } + finally { + _cacheLock.Release(); + } + } + + public async Task 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 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(); + } +} diff --git a/src/LetsEncryptServer/Services/CertsFlowService.cs b/src/LetsEncryptServer/Services/CertsFlowService.cs index bb2646e..313d9ce 100644 --- a/src/LetsEncryptServer/Services/CertsFlowService.cs +++ b/src/LetsEncryptServer/Services/CertsFlowService.cs @@ -32,17 +32,22 @@ public class CertsFlowService : ICertsFlowService { private readonly Configuration _appSettings; private readonly ILogger _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 appSettings, ILogger 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(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 : abort a transaction for a certificate file - add acl [@] : add an acl entry - add map [@] : add a map entry (payload supported instead of key/val) - add ssl crt-list [opts]* : add to crt-list file a line or a payload - clear acl [@] : clear the contents of this acl - clear counters [all] : clear max statistics counters (or all counters) - clear map [@] : clear the contents of this map - clear table []* : remove an entry from a table (filter: data/key) - commit acl @ : commit the ACL at this version - commit map @ : commit the map at this version - commit ssl cert : commit a certificate file - del acl [|#] : delete acl entries matching - del map [|#] : delete map entries matching - del ssl cert : delete an unused certificate file - del ssl crt-list : delete a line from crt-list file - disable agent : disable agent checks - disable dynamic-cookie backend : disable dynamic cookies on a specific backend - disable 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 : enable dynamic cookies on a specific backend - enable frontend : re-enable specific frontend - enable health : enable health checks - enable server (DEPRECATED) : enable a disabled server (use 'set server' instead) - get acl : report the patterns matching a sample for an ACL - get map : report the keys and values matching a sample for a map - get var : retrieve contents of a process-wide variable - get weight / : report a server's current weight - new ssl cert : 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 : prepare a new version for atomic ACL replacement - prepare map : prepare a new version for atomic map replacement - set dynamic-cookie-key backend : change a backend secret key for dynamic cookies - set map [|#] : modify a map entry - set maxconn frontend : change a frontend's maxconn setting - set maxconn global : change the per-process maxconn setting - set maxconn server / : change a server's maxconn setting - set profiling {auto|on|off} : enable/disable resource profiling (tasks,memory) - set rate-limit : change a rate limiting value - set server / [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 : replace a certificate file - set ssl ocsp-response : update a certificate's OCSP Response from a base64-encode DER - set ssl tls-key [id|file] : set the next TLS key for the or listener to - set table
key [data.* ]* : update or create a table entry's data - set timeout [cli] : change a timeout setting - set weight / (DEPRECATED) : change a server's weight (use 'set server' instead) - show 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 [] [request|response] : report last request and/or response errors for each proxy - show events [] [-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 [|<#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 [] : dump server connections status (all or for a single backend) - show servers state [] : 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 [] : display the SSL certificates used in memory, or the details of a file - show ssl crt-list [-n] [] : show the list of crt-lists or the content of a crt-list file - 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
[]* : 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 [] : show live tracing state - show version : show version of the current process - shutdown frontend : stop a specific frontend - shutdown session [id] : kill a specific session - shutdown sessions server / : kill sessions on a server - trace [|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 [] : list matching or all commands - prompt : toggle interactive mode with prompt - quit : disconnect - */ private IDomainResult NotifyHaproxy(IEnumerable certFiles) { var server = _appSettings.Server; + try { using (var client = new TcpClient(server.Ip, server.SocketPort)) using (var networkStream = client.GetStream())