using Microsoft.Extensions.Options; using MaksIT.LetsEncrypt.Entities; using MaksIT.LetsEncrypt.Services; using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests; using MaksIT.LetsEncrypt.Entities.LetsEncrypt; using MaksIT.Results; namespace MaksIT.LetsEncryptServer.Services; public interface ICertsCommonService { Result GetTermsOfService(Guid sessionId); Task CompleteChallengesAsync(Guid sessionId); } public interface ICertsInternalService : ICertsCommonService { Task> ConfigureClientAsync(bool isStaging); Task> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts); Task?>> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType); Task GetOrderAsync(Guid sessionId, string[] hostnames); Task GetCertificatesAsync(Guid sessionId, string[] hostnames); Task?>> ApplyCertificatesAsync(Guid sessionId, string[] hostnames); Task RevokeCertificatesAsync(Guid sessionId, string[] hostnames); Task> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[] hostnames); Task FullRevocationFlow(bool isStaging, Guid accountId, string description, string[] contacts, string[] hostnames); } public interface ICertsRestService : ICertsCommonService { Task> ConfigureClientAsync(ConfigureClientRequest requestData); Task> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData); Task?>> NewOrderAsync(Guid sessionId, NewOrderRequest requestData); Task GetOrderAsync(Guid sessionId, GetOrderRequest requestData); Task GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData); Task?>> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData); Task RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData); } public interface ICertsRestChallengeService { Result AcmeChallenge(string fileName); } public interface ICertsFlowService : ICertsInternalService, ICertsRestService, ICertsRestChallengeService { } public class CertsFlowService : ICertsFlowService { private readonly Configuration _appSettings; private readonly ILogger _logger; private readonly ILetsEncryptService _letsEncryptService; private readonly ICacheService _cacheService; private readonly IAgentService _agentService; private readonly string _acmePath; public CertsFlowService( IOptions appSettings, ILogger logger, ILetsEncryptService letsEncryptService, ICacheService cashService, IAgentService agentService ) { _appSettings = appSettings.Value; _logger = logger; _letsEncryptService = letsEncryptService; _cacheService = cashService; _agentService = agentService; _acmePath = _appSettings.AcmeFolder; } #region Common methods public Result GetTermsOfService(Guid sessionId) { var result = _letsEncryptService.GetTermsOfServiceUri(sessionId); return result; } public async Task CompleteChallengesAsync(Guid sessionId) { return await _letsEncryptService.CompleteChallenges(sessionId); } #endregion #region Internal methods public async Task> ConfigureClientAsync(bool isStaging) { var sessionId = Guid.NewGuid(); var result = await _letsEncryptService.ConfigureClient(sessionId, isStaging); if (!result.IsSuccess) return result.ToResultOfType(default); return Result.Ok(sessionId); } public async Task> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts) { RegistrationCache? cache = null; if (accountId == null) { accountId = Guid.NewGuid(); } else { var cacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId.Value); if (!cacheResult.IsSuccess || cacheResult.Value == null) { accountId = Guid.NewGuid(); } else { cache = cacheResult.Value; } } var result = await _letsEncryptService.Init(sessionId, accountId.Value, description, contacts, cache); if (!result.IsSuccess) return result.ToResultOfType(default); return Result.Ok(accountId.Value); } public async Task?>> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType) { var orderResult = await _letsEncryptService.NewOrder(sessionId, hostnames, challengeType); if (!orderResult.IsSuccess || orderResult.Value == null) return orderResult.ToResultOfType?>(_ => null); var challenges = new List(); foreach (var kvp in orderResult.Value) { string[] splitToken = kvp.Value.Split('.'); File.WriteAllText(Path.Combine(_acmePath, splitToken[0]), kvp.Value); challenges.Add(splitToken[0]); } return Result?>.Ok(challenges); } public async Task GetCertificatesAsync(Guid sessionId, string[] hostnames) { foreach (var subject in hostnames) { var result = await _letsEncryptService.GetCertificate(sessionId, subject); if (!result.IsSuccess) return result; Thread.Sleep(1000); } var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId); if (!cacheResult.IsSuccess || cacheResult.Value == null) return cacheResult; var saveResult = await _cacheService.SaveToCacheAsync(cacheResult.Value.AccountId, cacheResult.Value); if (!saveResult.IsSuccess) return saveResult; return Result.Ok(); } public async Task GetOrderAsync(Guid sessionId, string[] hostnames) { return await _letsEncryptService.GetOrder(sessionId, hostnames); } public async Task?>> ApplyCertificatesAsync(Guid sessionId, string[] hostnames) { var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId); if (!cacheResult.IsSuccess || cacheResult.Value?.CachedCerts == null) return cacheResult.ToResultOfType?>(_ => null); var results = new Dictionary(); foreach (var hostname in hostnames) { CertificateCache? cert; if (cacheResult.Value.TryGetCachedCertificate(hostname, out cert)) { var content = $"{cert.Cert}\n{cert.PrivatePem}"; results.Add(hostname, content); } } var uploadResult = await _agentService.UploadCerts(results); if (!uploadResult.IsSuccess) return uploadResult.ToResultOfType?>(default); var reloadResult = await _agentService.ReloadService(_appSettings.Agent.ServiceToReload); if (!reloadResult.IsSuccess) return reloadResult.ToResultOfType?>(default); return Result?>.Ok(results); } public async Task RevokeCertificatesAsync(Guid sessionId, string[] hostnames) { foreach (var hostname in hostnames) { var result = await _letsEncryptService.RevokeCertificate(sessionId, hostname, RevokeReason.Unspecified); if (!result.IsSuccess) return result; } var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId); if (!cacheResult.IsSuccess || cacheResult.Value == null) return cacheResult; var saveResult = await _cacheService.SaveToCacheAsync(cacheResult.Value.AccountId, cacheResult.Value); if (!saveResult.IsSuccess) return saveResult; return Result.Ok(); } public async Task> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[] hostnames) { var sessionResult = await ConfigureClientAsync(isStaging); if (!sessionResult.IsSuccess || sessionResult.Value == null) return sessionResult; var sessionId = sessionResult.Value.Value; var initResult = await InitAsync(sessionId, accountId, description, contacts); if (!initResult.IsSuccess) return initResult.ToResultOfType(_ => null); var challengesResult = await NewOrderAsync(sessionId, hostnames, challengeType); if (!challengesResult.IsSuccess) return challengesResult.ToResultOfType(_ => null); if (challengesResult.Value?.Count > 0) { var challengeResult = await CompleteChallengesAsync(sessionId); if (!challengeResult.IsSuccess) return challengeResult.ToResultOfType(default); } var getOrderResult = await GetOrderAsync(sessionId, hostnames); if (!getOrderResult.IsSuccess) return getOrderResult.ToResultOfType(default); var certsResult = await GetCertificatesAsync(sessionId, hostnames); if (!certsResult.IsSuccess) return certsResult.ToResultOfType(default); // Bypass applying certificates in staging mode if (!isStaging) { var applyCertsResult = await ApplyCertificatesAsync(sessionId, hostnames); if (!applyCertsResult.IsSuccess) return applyCertsResult.ToResultOfType(_ => null); } return Result.Ok(initResult.Value); } public async Task FullRevocationFlow(bool isStaging, Guid accountId, string description, string[] contacts, string[] hostnames) { var sessionResult = await ConfigureClientAsync(isStaging); if (!sessionResult.IsSuccess || sessionResult.Value == null) return sessionResult; var sessionId = sessionResult.Value.Value; var initResult = await InitAsync(sessionId, accountId, description, contacts); if (!initResult.IsSuccess) return initResult; var revokeResult = await RevokeCertificatesAsync(sessionId, hostnames); if (!revokeResult.IsSuccess) return revokeResult; return Result.Ok(); } #endregion #region REST methods public async Task> ConfigureClientAsync(ConfigureClientRequest requestData) { return await ConfigureClientAsync(requestData.IsStaging); } public async Task> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData) { return await InitAsync(sessionId, accountId, requestData.Description, requestData.Contacts); } public async Task>> NewOrderAsync(Guid sessionId, NewOrderRequest requestData) { return await NewOrderAsync(sessionId, requestData.Hostnames, requestData.ChallengeType); } public async Task GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) { return await GetCertificatesAsync(sessionId, requestData.Hostnames); } public async Task GetOrderAsync(Guid sessionId, GetOrderRequest requestData) { return await GetOrderAsync(sessionId, requestData.Hostnames); } public async Task>> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) => await ApplyCertificatesAsync(sessionId, requestData.Hostnames); public async Task RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData) => await RevokeCertificatesAsync(sessionId, requestData.Hostnames); #endregion #region Acme Challenge REST methods public Result AcmeChallenge(string fileName) { DeleteExporedChallenges(); var challengePath = Path.Combine(_acmePath, fileName); if (!File.Exists(challengePath)) return Result.NotFound(null); var fileContent = File.ReadAllText(Path.Combine(_acmePath, fileName)); return Result.Ok(fileContent); } private void DeleteExporedChallenges() { var currentDate = DateTime.Now; foreach (var file in Directory.GetFiles(_acmePath)) { try { var creationTime = File.GetCreationTime(file); var timeDifference = currentDate - creationTime; if (timeDifference.TotalDays > 1) { File.Delete(file); _logger.LogInformation($"Deleted file: {file}"); } } catch (Exception ex) { _logger.LogWarning(ex, "File cannot be deleted"); } } } #endregion }