From d59ded5cde09706c35ca473e29eddf3db3ba8127 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Sat, 1 Nov 2025 22:17:31 +0100 Subject: [PATCH] (feature): dependencies update, improved logging, allign naming to k8s deployment --- .../AuthorizationChallengeChallenge.cs | 10 +++++ .../Responses/AuthorizationChallengeError.cs | 12 ++++++ .../AuthorizationChallengeValidationRecord.cs | 20 +++++++++ .../Services/LetsEncryptService.cs | 18 ++++++-- .../Middlewares/GlobalExceptionMiddleware.cs | 32 --------------- src/LetsEncryptServer/Program.cs | 19 +++++---- .../Services/CertsFlowService.cs | 41 +++++++++++++++---- src/docker-compose.override.yml | 12 +++--- src/docker-compose.yml | 16 ++++---- 9 files changed, 115 insertions(+), 65 deletions(-) create mode 100644 src/LetsEncrypt/Models/Responses/AuthorizationChallengeError.cs create mode 100644 src/LetsEncrypt/Models/Responses/AuthorizationChallengeValidationRecord.cs delete mode 100644 src/LetsEncryptServer/Middlewares/GlobalExceptionMiddleware.cs diff --git a/src/LetsEncrypt/Models/Responses/AuthorizationChallengeChallenge.cs b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeChallenge.cs index 3b90d16..125f80e 100644 --- a/src/LetsEncrypt/Models/Responses/AuthorizationChallengeChallenge.cs +++ b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeChallenge.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace MaksIT.LetsEncrypt.Models.Responses; public class AuthorizationChallengeChallenge @@ -10,4 +11,13 @@ public class AuthorizationChallengeChallenge public string? Status { get; set; } public string? Token { get; set; } + + // New properties added to complete the model + public DateTime? Validated { get; set; } + + public AuthorizationChallengeError? Error { get; set; } + + public List? ValidationRecord { get; set; } } + + diff --git a/src/LetsEncrypt/Models/Responses/AuthorizationChallengeError.cs b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeError.cs new file mode 100644 index 0000000..b581c59 --- /dev/null +++ b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeError.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MaksIT.LetsEncrypt.Models.Responses; +public class AuthorizationChallengeError { + public string Type { get; set; } + + public string Detail { get; set; } +} diff --git a/src/LetsEncrypt/Models/Responses/AuthorizationChallengeValidationRecord.cs b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeValidationRecord.cs new file mode 100644 index 0000000..ce03961 --- /dev/null +++ b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeValidationRecord.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MaksIT.LetsEncrypt.Models.Responses; + + +public class AuthorizationChallengeValidationRecord { + public Uri? Url { get; set; } + + public string? Hostname { get; set; } + + public string? Port { get; set; } + + public List? AddressesResolved { get; set; } + + public string? AddressUsed { get; set; } +} \ No newline at end of file diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs index 71034f4..7264435 100644 --- a/src/LetsEncrypt/Services/LetsEncryptService.cs +++ b/src/LetsEncrypt/Services/LetsEncryptService.cs @@ -391,7 +391,7 @@ public class LetsEncryptService : ILetsEncryptService { if (challenge?.Url == null) { _logger.LogError("Challenge URL is null"); - return Result.InternalServerError(); + return Result.InternalServerError("Challenge URL is null"); } var request = new HttpRequestMessage(HttpMethod.Post, challenge.Url); @@ -712,6 +712,18 @@ public class LetsEncryptService : ILetsEncryptService { throw new LetsEncrytException(problem, response); } + + if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.Json)) { + var authorizationChallengeChallenge = responseText.ToObject(); + + if (authorizationChallengeChallenge?.Status == "invalid") { + throw new LetsEncrytException(new Problem { + Type = authorizationChallengeChallenge.Error.Type, + Detail = authorizationChallengeChallenge.Error.Detail, + RawJson = responseText + }, response); + } + } } private SendResult ProcessResponseContent(HttpResponseMessage response, string responseText) { @@ -743,14 +755,14 @@ public class LetsEncryptService : ILetsEncryptService { private Result HandleUnhandledException(Exception ex, string defaultMessage = "Let's Encrypt client unhandled exception") { List messages = new() { defaultMessage }; _logger.LogError(ex, messages.FirstOrDefault()); - messages.Add(ex.Message); + ex.ExtractMessages().ForEach(m => messages.Add(m)); return Result.InternalServerError([.. messages]); } private Result HandleUnhandledException(Exception ex, T? defaultValue = default, string defaultMessage = "Let's Encrypt client unhandled exception") { List messages = new() { defaultMessage }; _logger.LogError(ex, messages.FirstOrDefault()); - messages.Add(ex.Message); + ex.ExtractMessages().ForEach(m => messages.Add(m)); return Result.InternalServerError(defaultValue, [.. messages]); } } diff --git a/src/LetsEncryptServer/Middlewares/GlobalExceptionMiddleware.cs b/src/LetsEncryptServer/Middlewares/GlobalExceptionMiddleware.cs deleted file mode 100644 index 49574e2..0000000 --- a/src/LetsEncryptServer/Middlewares/GlobalExceptionMiddleware.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Net; - -namespace MaksIT.LetsEncryptServer.Middlewares { - public class GlobalExceptionMiddleware { - - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public GlobalExceptionMiddleware(RequestDelegate next, ILogger logger) { - _next = next; - _logger = logger; - } - - public async Task InvokeAsync(HttpContext context) { - try { - await _next(context); - } - catch (Exception ex) { - _logger.LogError(ex, "An unhandled exception occurred."); - await HandleExceptionAsync(context); - } - } - - private static Task HandleExceptionAsync(HttpContext context) { - context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - context.Response.ContentType = "application/json"; - - var response = new { message = "An error occurred while processing your request." }; - return context.Response.WriteAsJsonAsync(response); - } - } -} diff --git a/src/LetsEncryptServer/Program.cs b/src/LetsEncryptServer/Program.cs index b1b43fc..f608432 100644 --- a/src/LetsEncryptServer/Program.cs +++ b/src/LetsEncryptServer/Program.cs @@ -1,9 +1,10 @@ -using MaksIT.LetsEncryptServer; -using MaksIT.LetsEncrypt.Services; -using MaksIT.LetsEncryptServer.Services; -using MaksIT.LetsEncryptServer.BackgroundServices; -using MaksIT.LetsEncryptServer.Middlewares; +using MaksIT.Core.Webapi.Middlewares; +using MaksIT.Core.Logging; using MaksIT.LetsEncrypt.Extensions; +using MaksIT.LetsEncrypt.Services; +using MaksIT.LetsEncryptServer; +using MaksIT.LetsEncryptServer.BackgroundServices; +using MaksIT.LetsEncryptServer.Services; var builder = WebApplication.CreateBuilder(args); @@ -24,6 +25,9 @@ if (File.Exists(secretsPath)) { var configurationSection = configuration.GetSection("Configuration"); var appSettings = configurationSection.Get() ?? throw new ArgumentNullException(); +// Add logging +builder.Logging.AddConsoleLogger(); + // Allow configurations to be available through IOptions builder.Services.Configure(configurationSection); @@ -55,11 +59,8 @@ if (app.Environment.IsDevelopment()) { app.UseSwaggerUI(); app.UseCors(builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); } -else { - // app.UseMiddleware(); -} -app.UseMiddleware(); +app.UseMiddleware(); app.UseAuthorization(); diff --git a/src/LetsEncryptServer/Services/CertsFlowService.cs b/src/LetsEncryptServer/Services/CertsFlowService.cs index 344e4b8..6757a9a 100644 --- a/src/LetsEncryptServer/Services/CertsFlowService.cs +++ b/src/LetsEncryptServer/Services/CertsFlowService.cs @@ -81,9 +81,11 @@ public class CertsFlowService : ICertsFlowService { #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); } @@ -91,17 +93,22 @@ public class CertsFlowService : ICertsFlowService { RegistrationCache? cache = null; if (accountId == null) { accountId = Guid.NewGuid(); - } else { + } + else { var cacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId.Value); + if (!cacheResult.IsSuccess || cacheResult.Value == null) { accountId = Guid.NewGuid(); - } else { + } + 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); } @@ -109,12 +116,15 @@ public class CertsFlowService : ICertsFlowService { 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); } @@ -125,12 +135,15 @@ public class CertsFlowService : ICertsFlowService { 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(); } @@ -142,20 +155,25 @@ public class CertsFlowService : ICertsFlowService { 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); } @@ -165,12 +183,15 @@ public class CertsFlowService : ICertsFlowService { 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(); } @@ -194,6 +215,7 @@ public class CertsFlowService : ICertsFlowService { if (!challengeResult.IsSuccess) return challengeResult.ToResultOfType(default); } + var getOrderResult = await GetOrderAsync(sessionId, hostnames); if (!getOrderResult.IsSuccess) return getOrderResult.ToResultOfType(default); @@ -203,10 +225,10 @@ public class CertsFlowService : ICertsFlowService { 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); + if (!isStaging) { + var applyCertsResult = await ApplyCertificatesAsync(sessionId, hostnames); + if (!applyCertsResult.IsSuccess) + return applyCertsResult.ToResultOfType(_ => null); } return Result.Ok(initResult.Value); @@ -222,9 +244,11 @@ public class CertsFlowService : ICertsFlowService { 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 @@ -255,9 +279,11 @@ public class CertsFlowService : ICertsFlowService { #region Acme Challenge REST methods public Result AcmeChallenge(string fileName) { DeleteExporedChallenges(); + var challengePath = Path.Combine(_acmePath, fileName); - if(!File.Exists(challengePath)) + if (!File.Exists(challengePath)) return Result.NotFound(null); + var fileContent = File.ReadAllText(Path.Combine(_acmePath, fileName)); return Result.Ok(fileContent); } @@ -268,6 +294,7 @@ public class CertsFlowService : ICertsFlowService { try { var creationTime = File.GetCreationTime(file); var timeDifference = currentDate - creationTime; + if (timeDifference.TotalDays > 1) { File.Delete(file); _logger.LogInformation($"Deleted file: {file}"); diff --git a/src/docker-compose.override.yml b/src/docker-compose.override.yml index dbc4732..96bb46a 100644 --- a/src/docker-compose.override.yml +++ b/src/docker-compose.override.yml @@ -4,21 +4,21 @@ services: ports: - "8080:8080" depends_on: - - letsencrypt-app - - letsencrypt-server + - certs-ui-client + - certs-ui-server networks: - maks-it - letsencrypt-app: - container_name: letsencrypt-app + certs-ui-client: + container_name: certs-ui-client environment: - ASPNETCORE_ENVIRONMENT=Development - LETSENCRYPT_SERVER=http://localhost:8080 networks: - maks-it - letsencrypt-server: - container_name: letsencrypt-server + certs-ui-server: + container_name: certs-ui-server environment: - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_HTTP_PORTS=5000 diff --git a/src/docker-compose.yml b/src/docker-compose.yml index 9e8bea2..b5407cc 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -1,18 +1,18 @@ services: - letsencrypt-app: - image: ${DOCKER_REGISTRY-}letsencrypt-app - build: - context: . - dockerfile: ClientApp/Dockerfile - reverse-proxy: image: ${DOCKER_REGISTRY-}reverse-proxy build: context: . dockerfile: ReverseProxy/Dockerfile - letsencrypt-server: - image: ${DOCKER_REGISTRY-}letsencrypt-server + certs-ui-client: + image: ${DOCKER_REGISTRY-}certs-ui-client + build: + context: . + dockerfile: ClientApp/Dockerfile + + certs-ui-server: + image: ${DOCKER_REGISTRY-}certs-ui-server build: context: . dockerfile: LetsEncryptServer/Dockerfile