mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): migrate to MaksIT.Results
This commit is contained in:
parent
30f5ededa3
commit
7c962a4924
@ -8,7 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DomainResult.Common" Version="3.3.1" />
|
<PackageReference Include="MaksIT.Results" Version="1.0.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
||||||
|
|||||||
@ -4,37 +4,35 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
using System.Text;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using DomainResults.Common;
|
|
||||||
|
|
||||||
using MaksIT.LetsEncrypt.Entities;
|
|
||||||
using MaksIT.LetsEncrypt.Exceptions;
|
|
||||||
using MaksIT.Core.Extensions;
|
using MaksIT.Core.Extensions;
|
||||||
using MaksIT.LetsEncrypt.Models.Responses;
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
using MaksIT.LetsEncrypt.Models.Interfaces;
|
|
||||||
using MaksIT.LetsEncrypt.Models.Requests;
|
|
||||||
using MaksIT.LetsEncrypt.Entities.Jws;
|
using MaksIT.LetsEncrypt.Entities.Jws;
|
||||||
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
|
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
|
||||||
|
using MaksIT.LetsEncrypt.Exceptions;
|
||||||
|
using MaksIT.LetsEncrypt.Models.Interfaces;
|
||||||
|
using MaksIT.LetsEncrypt.Models.Requests;
|
||||||
|
using MaksIT.LetsEncrypt.Models.Responses;
|
||||||
|
using MaksIT.Results;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncrypt.Services;
|
namespace MaksIT.LetsEncrypt.Services;
|
||||||
|
|
||||||
public interface ILetsEncryptService {
|
public interface ILetsEncryptService {
|
||||||
Task<IDomainResult> ConfigureClient(Guid sessionId, bool isStaging);
|
Task<Result> ConfigureClient(Guid sessionId, bool isStaging);
|
||||||
Task<IDomainResult> Init(Guid sessionId,Guid accountId, string description, string[] contacts, RegistrationCache? registrationCache);
|
Task<Result> Init(Guid sessionId,Guid accountId, string description, string[] contacts, RegistrationCache? registrationCache);
|
||||||
(RegistrationCache?, IDomainResult) GetRegistrationCache(Guid sessionId);
|
Result<RegistrationCache?> GetRegistrationCache(Guid sessionId);
|
||||||
(string?, IDomainResult) GetTermsOfServiceUri(Guid sessionId);
|
Result<string?> GetTermsOfServiceUri(Guid sessionId);
|
||||||
Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(Guid sessionId, string[] hostnames, string challengeType);
|
Task<Result<Dictionary<string, string>?>> NewOrder(Guid sessionId, string[] hostnames, string challengeType);
|
||||||
Task<IDomainResult> CompleteChallenges(Guid sessionId);
|
Task<Result> CompleteChallenges(Guid sessionId);
|
||||||
Task<IDomainResult> GetOrder(Guid sessionId, string[] hostnames);
|
Task<Result> GetOrder(Guid sessionId, string[] hostnames);
|
||||||
Task<IDomainResult> GetCertificate(Guid sessionId, string subject);
|
Task<Result> GetCertificate(Guid sessionId, string subject);
|
||||||
Task<IDomainResult> RevokeCertificate(Guid sessionId, string subject, RevokeReason reason);
|
Task<Result> RevokeCertificate(Guid sessionId, string subject, RevokeReason reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LetsEncryptService : ILetsEncryptService {
|
public class LetsEncryptService : ILetsEncryptService {
|
||||||
@ -77,8 +75,8 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Poll challenge status until valid or timeout
|
// Helper: Poll challenge status until valid or timeout
|
||||||
private async Task<IDomainResult> PollChallengeStatus(Guid sessionId, AuthorizationChallengeChallenge challenge, State state) {
|
private async Task<Result> PollChallengeStatus(Guid sessionId, AuthorizationChallengeChallenge challenge, State state) {
|
||||||
if (challenge?.Url == null) return IDomainResult.Failed("Challenge URL is null");
|
if (challenge?.Url == null) return Result.InternalServerError("Challenge URL is null");
|
||||||
var start = DateTime.UtcNow;
|
var start = DateTime.UtcNow;
|
||||||
while (true) {
|
while (true) {
|
||||||
var pollRequest = new HttpRequestMessage(HttpMethod.Post, challenge.Url);
|
var pollRequest = new HttpRequestMessage(HttpMethod.Post, challenge.Url);
|
||||||
@ -94,15 +92,15 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
HandleProblemResponseAsync(pollResponse, pollResponseText);
|
HandleProblemResponseAsync(pollResponse, pollResponseText);
|
||||||
var authChallenge = ProcessResponseContent<AuthorizationChallengeResponse>(pollResponse, pollResponseText);
|
var authChallenge = ProcessResponseContent<AuthorizationChallengeResponse>(pollResponse, pollResponseText);
|
||||||
if (authChallenge.Result?.Status != "pending")
|
if (authChallenge.Result?.Status != "pending")
|
||||||
return authChallenge.Result?.Status == "valid" ? IDomainResult.Success() : IDomainResult.Failed();
|
return authChallenge.Result?.Status == "valid" ? Result.Ok() : Result.InternalServerError();
|
||||||
if ((DateTime.UtcNow - start).Seconds > 120)
|
if ((DateTime.UtcNow - start).Seconds > 120)
|
||||||
return IDomainResult.Failed("Timeout");
|
return Result.InternalServerError("Timeout");
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region ConfigureClient
|
#region ConfigureClient
|
||||||
public async Task<IDomainResult> ConfigureClient(Guid sessionId, bool isStaging) {
|
public async Task<Result> ConfigureClient(Guid sessionId, bool isStaging) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
state.IsStaging = isStaging;
|
state.IsStaging = isStaging;
|
||||||
@ -113,29 +111,29 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
var directory = await SendAcmeRequest<AcmeDirectory>(request, state, HttpMethod.Get);
|
var directory = await SendAcmeRequest<AcmeDirectory>(request, state, HttpMethod.Get);
|
||||||
state.Directory = directory.Result ?? throw new InvalidOperationException("Directory response is null");
|
state.Directory = directory.Result ?? throw new InvalidOperationException("Directory response is null");
|
||||||
}
|
}
|
||||||
return IDomainResult.Success();
|
return Result.Ok();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
const string message = "Let's Encrypt client unhandled exception";
|
const string message = "Let's Encrypt client unhandled exception";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError(message);
|
return Result.InternalServerError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Init
|
#region Init
|
||||||
public async Task<IDomainResult> Init(Guid sessionId, Guid accountId, string description, string[] contacts, RegistrationCache? cache) {
|
public async Task<Result> Init(Guid sessionId, Guid accountId, string description, string[] contacts, RegistrationCache? cache) {
|
||||||
if (sessionId == Guid.Empty) {
|
if (sessionId == Guid.Empty) {
|
||||||
_logger.LogError("Invalid sessionId");
|
_logger.LogError("Invalid sessionId");
|
||||||
return IDomainResult.Failed();
|
return Result.InternalServerError();
|
||||||
}
|
}
|
||||||
if (contacts == null || contacts.Length == 0) {
|
if (contacts == null || contacts.Length == 0) {
|
||||||
_logger.LogError("Contacts are null or empty");
|
_logger.LogError("Contacts are null or empty");
|
||||||
return IDomainResult.Failed();
|
return Result.InternalServerError();
|
||||||
}
|
}
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
if (state.Directory == null) {
|
if (state.Directory == null) {
|
||||||
_logger.LogError("State directory is null");
|
_logger.LogError("State directory is null");
|
||||||
return IDomainResult.Failed();
|
return Result.InternalServerError();
|
||||||
}
|
}
|
||||||
_logger.LogInformation($"Executing {nameof(Init)}...");
|
_logger.LogInformation($"Executing {nameof(Init)}...");
|
||||||
try {
|
try {
|
||||||
@ -162,7 +160,7 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
state.JwsService.SetKeyId(result.Result?.Location?.ToString() ?? string.Empty);
|
state.JwsService.SetKeyId(result.Result?.Location?.ToString() ?? string.Empty);
|
||||||
if (result.Result?.Status != "valid") {
|
if (result.Result?.Status != "valid") {
|
||||||
_logger.LogError($"Account status is not valid, was: {result.Result?.Status} \r\n {result.ResponseText}");
|
_logger.LogError($"Account status is not valid, was: {result.Result?.Status} \r\n {result.ResponseText}");
|
||||||
return IDomainResult.Failed();
|
return Result.InternalServerError();
|
||||||
}
|
}
|
||||||
state.Cache = new RegistrationCache {
|
state.Cache = new RegistrationCache {
|
||||||
AccountId = accountId,
|
AccountId = accountId,
|
||||||
@ -175,41 +173,41 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
Key = result.Result.Key
|
Key = result.Result.Key
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return IDomainResult.Success();
|
return Result.Ok();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
const string message = "Let's Encrypt client unhandled exception";
|
const string message = "Let's Encrypt client unhandled exception";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError(message);
|
return Result.InternalServerError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public (RegistrationCache?, IDomainResult) GetRegistrationCache(Guid sessionId) {
|
public Result<RegistrationCache?> GetRegistrationCache(Guid sessionId) {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
if(state?.Cache == null)
|
if(state?.Cache == null)
|
||||||
return IDomainResult.Failed<RegistrationCache?>();
|
return Result<RegistrationCache?>.InternalServerError(null);
|
||||||
return IDomainResult.Success(state.Cache);
|
return Result<RegistrationCache?>.Ok(state.Cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region GetTermsOfService
|
#region GetTermsOfService
|
||||||
public (string?, IDomainResult) GetTermsOfServiceUri(Guid sessionId) {
|
public Result<string?> GetTermsOfServiceUri(Guid sessionId) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
_logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}...");
|
_logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}...");
|
||||||
if (state.Directory?.Meta?.TermsOfService == null) {
|
if (state.Directory?.Meta?.TermsOfService == null) {
|
||||||
return IDomainResult.Failed<string?>();
|
return Result<string?>.Ok(null);
|
||||||
}
|
}
|
||||||
return IDomainResult.Success(state.Directory.Meta.TermsOfService);
|
return Result<string?>.Ok(state.Directory.Meta.TermsOfService);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError<string?>(message);
|
return Result<string?>.InternalServerError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region NewOrder
|
#region NewOrder
|
||||||
public async Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(Guid sessionId, string[] hostnames, string challengeType) {
|
public async Task<Result<Dictionary<string, string>?>> NewOrder(Guid sessionId, string[] hostnames, string challengeType) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
_logger.LogInformation($"Executing {nameof(NewOrder)}...");
|
_logger.LogInformation($"Executing {nameof(NewOrder)}...");
|
||||||
@ -222,7 +220,7 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
}).ToArray() ?? Array.Empty<OrderIdentifier>()
|
}).ToArray() ?? Array.Empty<OrderIdentifier>()
|
||||||
};
|
};
|
||||||
if (state.Directory == null || state.Directory.NewOrder == null)
|
if (state.Directory == null || state.Directory.NewOrder == null)
|
||||||
return (null, IDomainResult.Failed());
|
return Result<Dictionary<string, string>?>.InternalServerError(null);
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewOrder);
|
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewOrder);
|
||||||
await HandleNonceAsync(sessionId, state.Directory.NewOrder, state);
|
await HandleNonceAsync(sessionId, state.Directory.NewOrder, state);
|
||||||
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
||||||
@ -232,10 +230,10 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||||
var order = await SendAcmeRequest<Order>(request, state, HttpMethod.Post);
|
var order = await SendAcmeRequest<Order>(request, state, HttpMethod.Post);
|
||||||
if (StatusEquals(order.Result?.Status, OrderStatus.Ready))
|
if (StatusEquals(order.Result?.Status, OrderStatus.Ready))
|
||||||
return (new Dictionary<string, string>(), IDomainResult.Success());
|
return Result<Dictionary<string, string>?>.Ok(new Dictionary<string, string>());
|
||||||
if (!StatusEquals(order.Result?.Status, OrderStatus.Pending)) {
|
if (!StatusEquals(order.Result?.Status, OrderStatus.Pending)) {
|
||||||
_logger.LogError($"Created new order and expected status '{OrderStatus.Pending.GetDisplayName()}', but got: {order.Result?.Status} \r\n {order.Result}");
|
_logger.LogError($"Created new order and expected status '{OrderStatus.Pending.GetDisplayName()}', but got: {order.Result?.Status} \r\n {order.Result}");
|
||||||
return (null, IDomainResult.Failed());
|
return Result<Dictionary<string, string>?>.InternalServerError(null);
|
||||||
}
|
}
|
||||||
state.CurrentOrder = order.Result;
|
state.CurrentOrder = order.Result;
|
||||||
var results = new Dictionary<string, string>();
|
var results = new Dictionary<string, string>();
|
||||||
@ -253,12 +251,12 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
continue;
|
continue;
|
||||||
if (!StatusEquals(challengeResponse.Result?.Status, OrderStatus.Pending)) {
|
if (!StatusEquals(challengeResponse.Result?.Status, OrderStatus.Pending)) {
|
||||||
_logger.LogError($"Expected authorization status '{OrderStatus.Pending.GetDisplayName()}', but got: {state.CurrentOrder?.Status} \r\n {challengeResponse.ResponseText}");
|
_logger.LogError($"Expected authorization status '{OrderStatus.Pending.GetDisplayName()}', but got: {state.CurrentOrder?.Status} \r\n {challengeResponse.ResponseText}");
|
||||||
return (null, IDomainResult.Failed());
|
return Result<Dictionary<string, string>?>.InternalServerError(null);
|
||||||
}
|
}
|
||||||
var challenge = challengeResponse.Result?.Challenges?.FirstOrDefault(x => x?.Type == challengeType);
|
var challenge = challengeResponse.Result?.Challenges?.FirstOrDefault(x => x?.Type == challengeType);
|
||||||
if (challenge == null || challenge.Token == null) {
|
if (challenge == null || challenge.Token == null) {
|
||||||
_logger.LogError("Challenge or token is null");
|
_logger.LogError("Challenge or token is null");
|
||||||
return (null, IDomainResult.Failed());
|
return Result<Dictionary<string, string>?>.InternalServerError(null);
|
||||||
}
|
}
|
||||||
state.Challenges.Add(challenge);
|
state.Challenges.Add(challenge);
|
||||||
if (state.Cache != null) state.Cache.ChallengeType = challengeType;
|
if (state.Cache != null) state.Cache.ChallengeType = challengeType;
|
||||||
@ -277,28 +275,29 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (results, IDomainResult.Success());
|
|
||||||
|
return Result<Dictionary<string, string>?>.Ok(results);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return (null, IDomainResult.CriticalDependencyError(message));
|
return Result<Dictionary<string, string>?>.InternalServerError(null, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region CompleteChallenges
|
#region CompleteChallenges
|
||||||
public async Task<IDomainResult> CompleteChallenges(Guid sessionId) {
|
public async Task<Result> CompleteChallenges(Guid sessionId) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
|
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
|
||||||
if (state.CurrentOrder?.Identifiers == null) {
|
if (state.CurrentOrder?.Identifiers == null) {
|
||||||
return IDomainResult.Failed("Current order identifiers are null");
|
return Result.InternalServerError("Current order identifiers are null");
|
||||||
}
|
}
|
||||||
for (var index = 0; index < state.Challenges.Count; index++) {
|
for (var index = 0; index < state.Challenges.Count; index++) {
|
||||||
var challenge = state.Challenges[index];
|
var challenge = state.Challenges[index];
|
||||||
if (challenge?.Url == null) {
|
if (challenge?.Url == null) {
|
||||||
_logger.LogError("Challenge URL is null");
|
_logger.LogError("Challenge URL is null");
|
||||||
return IDomainResult.Failed();
|
return Result.InternalServerError();
|
||||||
}
|
}
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, challenge.Url);
|
var request = new HttpRequestMessage(HttpMethod.Post, challenge.Url);
|
||||||
await HandleNonceAsync(sessionId, challenge.Url, state);
|
await HandleNonceAsync(sessionId, challenge.Url, state);
|
||||||
@ -312,17 +311,17 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
if (!result.IsSuccess)
|
if (!result.IsSuccess)
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
return IDomainResult.Success();
|
return Result.Ok();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError(message);
|
return Result.InternalServerError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region GetOrder
|
#region GetOrder
|
||||||
public async Task<IDomainResult> GetOrder(Guid sessionId, string[] hostnames) {
|
public async Task<Result> GetOrder(Guid sessionId, string[] hostnames) {
|
||||||
try {
|
try {
|
||||||
_logger.LogInformation($"Executing {nameof(GetOrder)}");
|
_logger.LogInformation($"Executing {nameof(GetOrder)}");
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
@ -342,22 +341,22 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||||
var order = await SendAcmeRequest<Order>(request, state, HttpMethod.Post);
|
var order = await SendAcmeRequest<Order>(request, state, HttpMethod.Post);
|
||||||
state.CurrentOrder = order.Result;
|
state.CurrentOrder = order.Result;
|
||||||
return IDomainResult.Success();
|
return Result.Ok();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError(message);
|
return Result.InternalServerError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region GetCertificates
|
#region GetCertificates
|
||||||
public async Task<IDomainResult> GetCertificate(Guid sessionId, string subject) {
|
public async Task<Result> GetCertificate(Guid sessionId, string subject) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
_logger.LogInformation($"Executing {nameof(GetCertificate)}...");
|
_logger.LogInformation($"Executing {nameof(GetCertificate)}...");
|
||||||
if (state.CurrentOrder?.Identifiers == null) {
|
if (state.CurrentOrder?.Identifiers == null) {
|
||||||
return IDomainResult.Failed();
|
return Result.InternalServerError();
|
||||||
}
|
}
|
||||||
var key = new RSACryptoServiceProvider(4096);
|
var key = new RSACryptoServiceProvider(4096);
|
||||||
var csr = new CertificateRequest("CN=" + subject,
|
var csr = new CertificateRequest("CN=" + subject,
|
||||||
@ -417,7 +416,7 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
var pem = await SendAcmeRequest<string>(finalRequest, state, HttpMethod.Post);
|
var pem = await SendAcmeRequest<string>(finalRequest, state, HttpMethod.Post);
|
||||||
if (state.Cache == null) {
|
if (state.Cache == null) {
|
||||||
_logger.LogError($"{nameof(state.Cache)} is null");
|
_logger.LogError($"{nameof(state.Cache)} is null");
|
||||||
return IDomainResult.Failed();
|
return Result.InternalServerError();
|
||||||
}
|
}
|
||||||
state.Cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
|
state.Cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
|
||||||
state.Cache.CachedCerts[subject] = new CertificateCache {
|
state.Cache.CachedCerts[subject] = new CertificateCache {
|
||||||
@ -429,31 +428,31 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
if (!string.IsNullOrEmpty(certPem)) {
|
if (!string.IsNullOrEmpty(certPem)) {
|
||||||
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(certPem));
|
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(certPem));
|
||||||
}
|
}
|
||||||
return IDomainResult.Success();
|
return Result.Ok();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError(message);
|
return Result.InternalServerError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public Task<IDomainResult> KeyChange(Guid sessionId) {
|
public Task<Result> KeyChange(Guid sessionId) {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDomainResult> RevokeCertificate(Guid sessionId, string subject, RevokeReason reason) {
|
public async Task<Result> RevokeCertificate(Guid sessionId, string subject, RevokeReason reason) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
_logger.LogInformation($"Executing {nameof(RevokeCertificate)}...");
|
_logger.LogInformation($"Executing {nameof(RevokeCertificate)}...");
|
||||||
if (state.Cache?.CachedCerts == null || !state.Cache.CachedCerts.TryGetValue(subject, out var certificateCache) || certificateCache == null) {
|
if (state.Cache?.CachedCerts == null || !state.Cache.CachedCerts.TryGetValue(subject, out var certificateCache) || certificateCache == null) {
|
||||||
_logger.LogError("Certificate not found in cache");
|
_logger.LogError("Certificate not found in cache");
|
||||||
return IDomainResult.Failed("Certificate not found");
|
return Result.InternalServerError("Certificate not found");
|
||||||
}
|
}
|
||||||
var certPem = certificateCache.Cert ?? string.Empty;
|
var certPem = certificateCache.Cert ?? string.Empty;
|
||||||
if (string.IsNullOrEmpty(certPem)) {
|
if (string.IsNullOrEmpty(certPem)) {
|
||||||
_logger.LogError("Certificate PEM is null or empty");
|
_logger.LogError("Certificate PEM is null or empty");
|
||||||
return IDomainResult.Failed("Certificate PEM is null or empty");
|
return Result.InternalServerError("Certificate PEM is null or empty");
|
||||||
}
|
}
|
||||||
var certificate = new X509Certificate2(Encoding.UTF8.GetBytes(certPem));
|
var certificate = new X509Certificate2(Encoding.UTF8.GetBytes(certPem));
|
||||||
var derEncodedCert = certificate.Export(X509ContentType.Cert);
|
var derEncodedCert = certificate.Export(X509ContentType.Cert);
|
||||||
@ -478,14 +477,14 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
var erroObj = responseText.ToObject<Problem>();
|
var erroObj = responseText.ToObject<Problem>();
|
||||||
}
|
}
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
IDomainResult.CriticalDependencyError(responseText);
|
Result.InternalServerError(responseText);
|
||||||
state.Cache.CachedCerts.Remove(subject);
|
state.Cache.CachedCerts.Remove(subject);
|
||||||
_logger.LogInformation("Certificate revoked successfully");
|
_logger.LogInformation("Certificate revoked successfully");
|
||||||
return IDomainResult.Success();
|
return Result.Ok();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError($"{message}: {ex.Message}");
|
return Result.InternalServerError($"{message}: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -493,31 +492,31 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
private async Task HandleNonceAsync(Guid sessionId, Uri uri, State state) {
|
private async Task HandleNonceAsync(Guid sessionId, Uri uri, State state) {
|
||||||
if (uri == null) throw new ArgumentNullException(nameof(uri));
|
if (uri == null) throw new ArgumentNullException(nameof(uri));
|
||||||
if (uri.OriginalString != "directory") {
|
if (uri.OriginalString != "directory") {
|
||||||
var (nonce, newNonceResult) = await NewNonce(sessionId);
|
var newNonceResult = await NewNonce(sessionId);
|
||||||
if (!newNonceResult.IsSuccess || nonce == null) {
|
if (!newNonceResult.IsSuccess || newNonceResult.Value == null) {
|
||||||
throw new InvalidOperationException("Failed to retrieve nonce.");
|
throw new InvalidOperationException("Failed to retrieve nonce.");
|
||||||
}
|
}
|
||||||
state.Nonce = nonce;
|
state.Nonce = newNonceResult.Value;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
state.Nonce = default;
|
state.Nonce = default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(string?, IDomainResult)> NewNonce(Guid sessionId) {
|
private async Task<Result<string?>> NewNonce(Guid sessionId) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
_logger.LogInformation($"Executing {nameof(NewNonce)}...");
|
_logger.LogInformation($"Executing {nameof(NewNonce)}...");
|
||||||
if (state.Directory?.NewNonce == null)
|
if (state.Directory?.NewNonce == null)
|
||||||
return (null, IDomainResult.Failed());
|
return Result<string?>.InternalServerError(null);
|
||||||
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, state.Directory.NewNonce));
|
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, state.Directory.NewNonce));
|
||||||
var nonce = result.Headers.GetValues("Replay-Nonce").FirstOrDefault();
|
var nonce = result.Headers.GetValues("Replay-Nonce").FirstOrDefault();
|
||||||
return (nonce, IDomainResult.Success());
|
return Result<string?>.Ok(nonce);
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return (null, IDomainResult.CriticalDependencyError(message));
|
return Result<string?>.InternalServerError(null, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
using DomainResults.Common;
|
|
||||||
|
|
||||||
|
|
||||||
using MaksIT.LetsEncryptServer.Services;
|
using MaksIT.LetsEncryptServer.Services;
|
||||||
using MaksIT.LetsEncrypt.Entities;
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
using MaksIT.Results;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
||||||
public class AutoRenewal : BackgroundService {
|
public class AutoRenewal : BackgroundService {
|
||||||
@ -31,12 +29,14 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
|||||||
while (!stoppingToken.IsCancellationRequested) {
|
while (!stoppingToken.IsCancellationRequested) {
|
||||||
_logger.LogInformation("Background service is running.");
|
_logger.LogInformation("Background service is running.");
|
||||||
|
|
||||||
var (accountsResponse, getAccountIdsResult) = await _cacheService.LoadAccountsFromCacheAsync();
|
var loadAccountsFromCacheResult = await _cacheService.LoadAccountsFromCacheAsync();
|
||||||
if (!getAccountIdsResult.IsSuccess || accountsResponse == null) {
|
if (!loadAccountsFromCacheResult.IsSuccess || loadAccountsFromCacheResult.Value == null) {
|
||||||
LogErrors(getAccountIdsResult.Errors);
|
LogErrors(loadAccountsFromCacheResult.Messages);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var accountsResponse = loadAccountsFromCacheResult.Value;
|
||||||
|
|
||||||
foreach (var account in accountsResponse.Where(x => !x.IsDisabled)) {
|
foreach (var account in accountsResponse.Where(x => !x.IsDisabled)) {
|
||||||
await ProcessAccountAsync(account);
|
await ProcessAccountAsync(account);
|
||||||
}
|
}
|
||||||
@ -45,27 +45,27 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IDomainResult> ProcessAccountAsync(RegistrationCache cache) {
|
private async Task<Result> ProcessAccountAsync(RegistrationCache cache) {
|
||||||
|
|
||||||
var hostnames = cache.GetHostsWithUpcomingSslExpiry();
|
var hostnames = cache.GetHostsWithUpcomingSslExpiry();
|
||||||
if (hostnames == null) {
|
if (hostnames == null) {
|
||||||
_logger.LogError("Unexpected hostnames null");
|
_logger.LogError("Unexpected hostnames null");
|
||||||
return IDomainResult.Success();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!hostnames.Any()) {
|
if (!hostnames.Any()) {
|
||||||
_logger.LogInformation("No hosts found with upcoming SSL expiry");
|
_logger.LogInformation("No hosts found with upcoming SSL expiry");
|
||||||
return IDomainResult.Success();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
var (_, renewResult) = await _certsFlowService.FullFlow(cache.IsStaging, cache.AccountId, cache.Description, cache.Contacts, cache.ChallengeType, hostnames);
|
var fullFlowResult = await _certsFlowService.FullFlow(cache.IsStaging, cache.AccountId, cache.Description, cache.Contacts, cache.ChallengeType, hostnames);
|
||||||
if (!renewResult.IsSuccess)
|
if (!fullFlowResult.IsSuccess)
|
||||||
return renewResult;
|
return fullFlowResult;
|
||||||
|
|
||||||
_logger.LogInformation($"Certificates renewed for account {cache.AccountId}");
|
_logger.LogInformation($"Certificates renewed for account {cache.AccountId}");
|
||||||
|
|
||||||
return IDomainResult.Success();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
using DomainResults.Mvc;
|
|
||||||
|
|
||||||
using MaksIT.LetsEncryptServer.Services;
|
using MaksIT.LetsEncryptServer.Services;
|
||||||
using MaksIT.Models.LetsEncryptServer.Account.Requests;
|
using MaksIT.Models.LetsEncryptServer.Account.Requests;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
using DomainResults.Mvc;
|
|
||||||
|
|
||||||
using MaksIT.LetsEncryptServer.Services;
|
using MaksIT.LetsEncryptServer.Services;
|
||||||
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
using DomainResults.Mvc;
|
|
||||||
|
|
||||||
using MaksIT.LetsEncryptServer.Services;
|
using MaksIT.LetsEncryptServer.Services;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DomainResult" Version="3.3.1" />
|
<PackageReference Include="MaksIT.Results" Version="1.0.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
using DomainResults.Common;
|
|
||||||
|
|
||||||
using MaksIT.LetsEncrypt.Entities;
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
using MaksIT.Models;
|
using MaksIT.Models;
|
||||||
using MaksIT.Models.LetsEncryptServer.Account.Requests;
|
using MaksIT.Models.LetsEncryptServer.Account.Requests;
|
||||||
using MaksIT.Models.LetsEncryptServer.Account.Responses;
|
using MaksIT.Models.LetsEncryptServer.Account.Responses;
|
||||||
|
using MaksIT.Results;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Services;
|
namespace MaksIT.LetsEncryptServer.Services;
|
||||||
|
|
||||||
@ -14,11 +14,11 @@ public interface IAccountInternalService {
|
|||||||
|
|
||||||
|
|
||||||
public interface IAccountRestService {
|
public interface IAccountRestService {
|
||||||
Task<(GetAccountResponse[]?, IDomainResult)> GetAccountsAsync();
|
Task<Result<GetAccountResponse[]?>> GetAccountsAsync();
|
||||||
Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId);
|
Task<Result<GetAccountResponse?>> GetAccountAsync(Guid accountId);
|
||||||
Task<(GetAccountResponse?, IDomainResult)> PostAccountAsync(PostAccountRequest requestData);
|
Task<Result<GetAccountResponse?>> PostAccountAsync(PostAccountRequest requestData);
|
||||||
Task<(GetAccountResponse?, IDomainResult)> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData);
|
Task<Result<GetAccountResponse?>> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData);
|
||||||
Task<IDomainResult> DeleteAccountAsync(Guid accountId);
|
Task<Result> DeleteAccountAsync(Guid accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IAccountService : IAccountInternalService, IAccountRestService { }
|
public interface IAccountService : IAccountInternalService, IAccountRestService { }
|
||||||
@ -41,34 +41,37 @@ public class AccountService : IAccountService {
|
|||||||
|
|
||||||
#region Accounts
|
#region Accounts
|
||||||
|
|
||||||
public async Task<(GetAccountResponse[]?, IDomainResult)> GetAccountsAsync() {
|
public async Task<Result<GetAccountResponse[]?>> GetAccountsAsync() {
|
||||||
|
|
||||||
var (caches, result) = await _cacheService.LoadAccountsFromCacheAsync();
|
var accountsFromCacheResult = await _cacheService.LoadAccountsFromCacheAsync();
|
||||||
if (!result.IsSuccess || caches == null) {
|
if (!accountsFromCacheResult.IsSuccess || accountsFromCacheResult.Value == null) {
|
||||||
return (null, result);
|
return accountsFromCacheResult
|
||||||
|
.ToResultOfType<GetAccountResponse[]?>(_ => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var accounts = caches
|
var accounts = accountsFromCacheResult.Value
|
||||||
.Select(x => CreateGetAccountResponse(x.AccountId, x))
|
.Select(x => CreateGetAccountResponse(x.AccountId, x))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
return IDomainResult.Success(accounts);
|
return Result<GetAccountResponse[]?>.Ok(accounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId) {
|
public async Task<Result<GetAccountResponse?>> GetAccountAsync(Guid accountId) {
|
||||||
var (cache, result) = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
var loadFromCacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
||||||
if (!result.IsSuccess || cache == null) {
|
if (!loadFromCacheResult.IsSuccess || loadFromCacheResult.Value == null) {
|
||||||
return (null, result);
|
return loadFromCacheResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return IDomainResult.Success(CreateGetAccountResponse(accountId, cache));
|
var cache = loadFromCacheResult.Value;
|
||||||
|
|
||||||
|
return Result<GetAccountResponse?>.Ok(CreateGetAccountResponse(accountId, cache));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> PostAccountAsync(PostAccountRequest requestData) {
|
public async Task<Result<GetAccountResponse?>> PostAccountAsync(PostAccountRequest requestData) {
|
||||||
|
|
||||||
// TODO: check for overlapping hostnames in already existing accounts
|
// TODO: check for overlapping hostnames in already existing accounts
|
||||||
|
|
||||||
var (accountId, newCertsResult) = await _certsFlowService.FullFlow(
|
var fullFlowResult = await _certsFlowService.FullFlow(
|
||||||
requestData.IsStaging,
|
requestData.IsStaging,
|
||||||
null,
|
null,
|
||||||
requestData.Description,
|
requestData.Description,
|
||||||
@ -77,25 +80,31 @@ public class AccountService : IAccountService {
|
|||||||
requestData.Hostnames
|
requestData.Hostnames
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!newCertsResult.IsSuccess || accountId == null)
|
|
||||||
return (null, newCertsResult);
|
|
||||||
|
|
||||||
|
|
||||||
|
if (!fullFlowResult.IsSuccess || fullFlowResult.Value == null)
|
||||||
|
return fullFlowResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
||||||
|
|
||||||
var (cache, loadResult) = await _cacheService.LoadAccountFromCacheAsync(accountId.Value);
|
var accountId = fullFlowResult.Value.Value;
|
||||||
if (!loadResult.IsSuccess || cache == null) {
|
|
||||||
return (null, loadResult);
|
var loadAccauntFromCacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
||||||
|
if (!loadAccauntFromCacheResult.IsSuccess || loadAccauntFromCacheResult.Value == null) {
|
||||||
|
return loadAccauntFromCacheResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return IDomainResult.Success(CreateGetAccountResponse(accountId.Value, cache));
|
var cache = loadAccauntFromCacheResult.Value;
|
||||||
|
|
||||||
|
return Result<GetAccountResponse?>.Ok(CreateGetAccountResponse(accountId, cache));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData) {
|
public async Task<Result<GetAccountResponse?>> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData) {
|
||||||
var (cache, loadResult) = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
var loadAccountResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null) {
|
if (!loadAccountResult.IsSuccess || loadAccountResult.Value == null) {
|
||||||
return (null, loadResult);
|
return loadAccountResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cache = loadAccountResult.Value;
|
||||||
|
|
||||||
if (requestData.Description != null) {
|
if (requestData.Description != null) {
|
||||||
switch (requestData.Description.Op) {
|
switch (requestData.Description.Op) {
|
||||||
case PatchOperation.Replace:
|
case PatchOperation.Replace:
|
||||||
@ -172,11 +181,11 @@ public class AccountService : IAccountService {
|
|||||||
|
|
||||||
var saveResult = await _cacheService.SaveToCacheAsync(accountId, cache);
|
var saveResult = await _cacheService.SaveToCacheAsync(accountId, cache);
|
||||||
if (!saveResult.IsSuccess) {
|
if (!saveResult.IsSuccess) {
|
||||||
return (null, saveResult);
|
return saveResult.ToResultOfType<GetAccountResponse?>(default);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hostnamesToAdd.Count > 0) {
|
if (hostnamesToAdd.Count > 0) {
|
||||||
var (_, newCertsResult) = await _certsFlowService.FullFlow(
|
var fullFlowResult = await _certsFlowService.FullFlow(
|
||||||
cache.IsStaging,
|
cache.IsStaging,
|
||||||
cache.AccountId,
|
cache.AccountId,
|
||||||
cache.Description,
|
cache.Description,
|
||||||
@ -185,8 +194,8 @@ public class AccountService : IAccountService {
|
|||||||
hostnamesToAdd.ToArray()
|
hostnamesToAdd.ToArray()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!newCertsResult.IsSuccess)
|
if (!fullFlowResult.IsSuccess)
|
||||||
return (null, newCertsResult);
|
return fullFlowResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hostnamesToRemove.Count > 0) {
|
if (hostnamesToRemove.Count > 0) {
|
||||||
@ -199,18 +208,18 @@ public class AccountService : IAccountService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!revokeResult.IsSuccess)
|
if (!revokeResult.IsSuccess)
|
||||||
return (null, revokeResult);
|
return revokeResult.ToResultOfType<GetAccountResponse?>(default);
|
||||||
}
|
}
|
||||||
|
|
||||||
(cache, loadResult) = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
loadAccountResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null) {
|
if (!loadAccountResult.IsSuccess || loadAccountResult.Value == null) {
|
||||||
return (null, loadResult);
|
return loadAccountResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return IDomainResult.Success(CreateGetAccountResponse(accountId, cache));
|
return Result<GetAccountResponse?>.Ok(CreateGetAccountResponse(accountId, cache));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDomainResult> DeleteAccountAsync(Guid accountId) {
|
public async Task<Result> DeleteAccountAsync(Guid accountId) {
|
||||||
// TODO: Revoke all certificates
|
// TODO: Revoke all certificates
|
||||||
|
|
||||||
// Remove from cache
|
// Remove from cache
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
using DomainResults.Common;
|
using MaksIT.Models.Agent.Requests;
|
||||||
using MaksIT.Models.Agent.Requests;
|
using MaksIT.Results;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@ -7,9 +7,9 @@ using System.Text.Json;
|
|||||||
namespace MaksIT.LetsEncryptServer.Services {
|
namespace MaksIT.LetsEncryptServer.Services {
|
||||||
|
|
||||||
public interface IAgentService {
|
public interface IAgentService {
|
||||||
Task<IDomainResult> GetHelloWorld();
|
Task<Result> GetHelloWorld();
|
||||||
Task<IDomainResult> UploadCerts(Dictionary<string, string> certs);
|
Task<Result> UploadCerts(Dictionary<string, string> certs);
|
||||||
Task<IDomainResult> ReloadService(string serviceName);
|
Task<Result> ReloadService(string serviceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AgentService : IAgentService {
|
public class AgentService : IAgentService {
|
||||||
@ -28,23 +28,23 @@ namespace MaksIT.LetsEncryptServer.Services {
|
|||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IDomainResult> GetHelloWorld() {
|
public Task<Result> GetHelloWorld() {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDomainResult> ReloadService(string serviceName) {
|
public async Task<Result> ReloadService(string serviceName) {
|
||||||
var requestBody = new ServiceReloadRequest { ServiceName = serviceName };
|
var requestBody = new ServiceReloadRequest { ServiceName = serviceName };
|
||||||
var endpoint = $"/Service/Reload";
|
var endpoint = $"/Service/Reload";
|
||||||
return await SendHttpRequest(requestBody, endpoint);
|
return await SendHttpRequest(requestBody, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDomainResult> UploadCerts(Dictionary<string, string> certs) {
|
public async Task<Result> UploadCerts(Dictionary<string, string> certs) {
|
||||||
var requestBody = new CertsUploadRequest { Certs = certs };
|
var requestBody = new CertsUploadRequest { Certs = certs };
|
||||||
var endpoint = $"/Certs/Upload";
|
var endpoint = $"/Certs/Upload";
|
||||||
return await SendHttpRequest(requestBody, endpoint);
|
return await SendHttpRequest(requestBody, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IDomainResult> SendHttpRequest<T>(T requestBody, string endpoint) {
|
private async Task<Result> SendHttpRequest<T>(T requestBody, string endpoint) {
|
||||||
try {
|
try {
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, $"{_appSettings.Agent.AgentHostname}:{_appSettings.Agent.AgentPort}{endpoint}") {
|
var request = new HttpRequestMessage(HttpMethod.Post, $"{_appSettings.Agent.AgentHostname}:{_appSettings.Agent.AgentPort}{endpoint}") {
|
||||||
Content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json")
|
Content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json")
|
||||||
@ -56,16 +56,16 @@ namespace MaksIT.LetsEncryptServer.Services {
|
|||||||
var response = await _httpClient.SendAsync(request);
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode) {
|
if (response.IsSuccessStatusCode) {
|
||||||
return IDomainResult.Success();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
_logger.LogError($"Request to {endpoint} failed with status code: {response.StatusCode}");
|
_logger.LogError($"Request to {endpoint} failed with status code: {response.StatusCode}");
|
||||||
return IDomainResult.Failed($"Request to {endpoint} failed with status code: {response.StatusCode}");
|
return Result.InternalServerError($"Request to {endpoint} failed with status code: {response.StatusCode}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
_logger.LogError(ex, "Something went wrong");
|
_logger.LogError(ex, "Something went wrong");
|
||||||
return IDomainResult.Failed("Something went wrong");
|
return Result.InternalServerError("Something went wrong");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,17 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
using DomainResults.Common;
|
|
||||||
|
|
||||||
using MaksIT.Core.Extensions;
|
using MaksIT.Core.Extensions;
|
||||||
using MaksIT.LetsEncrypt.Entities;
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
using MaksIT.Models;
|
using MaksIT.Results;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Services;
|
namespace MaksIT.LetsEncryptServer.Services;
|
||||||
|
|
||||||
public interface ICacheService {
|
public interface ICacheService {
|
||||||
Task<(RegistrationCache[]?, IDomainResult)> LoadAccountsFromCacheAsync();
|
Task<Result<RegistrationCache[]?>> LoadAccountsFromCacheAsync();
|
||||||
Task<(RegistrationCache?, IDomainResult)> LoadAccountFromCacheAsync(Guid accountId);
|
Task<Result<RegistrationCache?>> LoadAccountFromCacheAsync(Guid accountId);
|
||||||
Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache);
|
Task<Result> SaveToCacheAsync(Guid accountId, RegistrationCache cache);
|
||||||
Task<IDomainResult> DeleteFromCacheAsync(Guid accountId);
|
Task<Result> DeleteFromCacheAsync(Guid accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CacheService : ICacheService, IDisposable {
|
public class CacheService : ICacheService, IDisposable {
|
||||||
@ -47,63 +46,58 @@ public class CacheService : ICacheService, IDisposable {
|
|||||||
|
|
||||||
#region Cache Operations
|
#region Cache Operations
|
||||||
|
|
||||||
public async Task<(RegistrationCache[]?, IDomainResult)> LoadAccountsFromCacheAsync() {
|
public async Task<Result<RegistrationCache[]?>> LoadAccountsFromCacheAsync() {
|
||||||
return await _lockManager.ExecuteWithLockAsync(async () => {
|
return await _lockManager.ExecuteWithLockAsync(async () => {
|
||||||
var accountIds = GetCachedAccounts();
|
var accountIds = GetCachedAccounts();
|
||||||
var cacheLoadTasks = accountIds.Select(accountId => LoadFromCacheInternalAsync(accountId)).ToList();
|
var cacheLoadTasks = accountIds.Select(accountId => LoadFromCacheInternalAsync(accountId)).ToList();
|
||||||
|
|
||||||
var caches = new List<RegistrationCache>();
|
var caches = new List<RegistrationCache>();
|
||||||
foreach (var task in cacheLoadTasks) {
|
foreach (var task in cacheLoadTasks) {
|
||||||
var (registrationCache, getRegistrationCacheResult) = await task;
|
var taskResult = await task;
|
||||||
if (!getRegistrationCacheResult.IsSuccess || registrationCache == null) {
|
if (!taskResult.IsSuccess || taskResult.Value == null) {
|
||||||
// Depending on how you want to handle partial failures, you might want to return here
|
// Depending on how you want to handle partial failures, you might want to return here
|
||||||
// or continue loading other caches. For now, let's continue.
|
// or continue loading other caches. For now, let's continue.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var registrationCache = taskResult.Value;
|
||||||
|
|
||||||
caches.Add(registrationCache);
|
caches.Add(registrationCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
return IDomainResult.Success(caches.ToArray());
|
return Result<RegistrationCache[]?>.Ok(caches.ToArray());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<Result<RegistrationCache?>> LoadFromCacheInternalAsync(Guid accountId) {
|
||||||
|
|
||||||
|
|
||||||
private async Task<(RegistrationCache?, IDomainResult)> LoadFromCacheInternalAsync(Guid accountId) {
|
|
||||||
var cacheFilePath = GetCacheFilePath(accountId);
|
var cacheFilePath = GetCacheFilePath(accountId);
|
||||||
|
|
||||||
if (!File.Exists(cacheFilePath)) {
|
if (!File.Exists(cacheFilePath)) {
|
||||||
var message = $"Cache file not found for account {accountId}";
|
var message = $"Cache file not found for account {accountId}";
|
||||||
_logger.LogWarning(message);
|
_logger.LogWarning(message);
|
||||||
return IDomainResult.Failed<RegistrationCache>(message);
|
return Result<RegistrationCache?>.InternalServerError(null, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
var json = await File.ReadAllTextAsync(cacheFilePath);
|
var json = await File.ReadAllTextAsync(cacheFilePath);
|
||||||
if (string.IsNullOrEmpty(json)) {
|
if (string.IsNullOrEmpty(json)) {
|
||||||
var message = $"Cache file is empty for account {accountId}";
|
var message = $"Cache file is empty for account {accountId}";
|
||||||
_logger.LogWarning(message);
|
_logger.LogWarning(message);
|
||||||
return IDomainResult.Failed<RegistrationCache>(message);
|
return Result<RegistrationCache?>.InternalServerError(null, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
var cache = JsonSerializer.Deserialize<RegistrationCache>(json);
|
var cache = JsonSerializer.Deserialize<RegistrationCache>(json);
|
||||||
return IDomainResult.Success(cache);
|
return Result<RegistrationCache?>.Ok(cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<Result> SaveToCacheInternalAsync(Guid accountId, RegistrationCache cache) {
|
||||||
|
|
||||||
private async Task<IDomainResult> SaveToCacheInternalAsync(Guid accountId, RegistrationCache cache) {
|
|
||||||
var cacheFilePath = GetCacheFilePath(accountId);
|
var cacheFilePath = GetCacheFilePath(accountId);
|
||||||
var json = JsonSerializer.Serialize(cache);
|
var json = JsonSerializer.Serialize(cache);
|
||||||
await File.WriteAllTextAsync(cacheFilePath, json);
|
await File.WriteAllTextAsync(cacheFilePath, json);
|
||||||
_logger.LogInformation($"Cache file saved for account {accountId}");
|
_logger.LogInformation($"Cache file saved for account {accountId}");
|
||||||
return DomainResult.Success();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Result DeleteFromCacheInternal(Guid accountId) {
|
||||||
|
|
||||||
private IDomainResult DeleteFromCacheInternal(Guid accountId) {
|
|
||||||
var cacheFilePath = GetCacheFilePath(accountId);
|
var cacheFilePath = GetCacheFilePath(accountId);
|
||||||
if (File.Exists(cacheFilePath)) {
|
if (File.Exists(cacheFilePath)) {
|
||||||
File.Delete(cacheFilePath);
|
File.Delete(cacheFilePath);
|
||||||
@ -112,22 +106,22 @@ public class CacheService : ICacheService, IDisposable {
|
|||||||
else {
|
else {
|
||||||
_logger.LogWarning($"Cache file not found for account {accountId}");
|
_logger.LogWarning($"Cache file not found for account {accountId}");
|
||||||
}
|
}
|
||||||
return DomainResult.Success();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
public async Task<Result<RegistrationCache?>> LoadAccountFromCacheAsync(Guid accountId) {
|
||||||
|
return await _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
|
||||||
|
|
||||||
public Task<(RegistrationCache?, IDomainResult)> LoadAccountFromCacheAsync(Guid accountId) {
|
|
||||||
return _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache) {
|
public async Task<Result> SaveToCacheAsync(Guid accountId, RegistrationCache cache) {
|
||||||
return _lockManager.ExecuteWithLockAsync(() => SaveToCacheInternalAsync(accountId, cache));
|
return await _lockManager.ExecuteWithLockAsync(() => SaveToCacheInternalAsync(accountId, cache));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IDomainResult> DeleteFromCacheAsync(Guid accountId) {
|
public async Task<Result> DeleteFromCacheAsync(Guid accountId) {
|
||||||
return _lockManager.ExecuteWithLockAsync(() => DeleteFromCacheInternal(accountId));
|
return await _lockManager.ExecuteWithLockAsync(() => DeleteFromCacheInternal(accountId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
|
|||||||
@ -1,47 +1,42 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
using DomainResults.Common;
|
|
||||||
|
|
||||||
using MaksIT.LetsEncrypt.Entities;
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
using MaksIT.LetsEncrypt.Services;
|
using MaksIT.LetsEncrypt.Services;
|
||||||
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
|
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
|
||||||
|
using MaksIT.Results;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Services;
|
namespace MaksIT.LetsEncryptServer.Services;
|
||||||
|
|
||||||
public interface ICertsCommonService {
|
public interface ICertsCommonService {
|
||||||
|
Result<string?> GetTermsOfService(Guid sessionId);
|
||||||
(string?, IDomainResult) GetTermsOfService(Guid sessionId);
|
Task<Result> CompleteChallengesAsync(Guid sessionId);
|
||||||
Task<IDomainResult> CompleteChallengesAsync(Guid sessionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ICertsInternalService : ICertsCommonService {
|
public interface ICertsInternalService : ICertsCommonService {
|
||||||
Task<(Guid?, IDomainResult)> ConfigureClientAsync(bool isStaging);
|
Task<Result<Guid?>> ConfigureClientAsync(bool isStaging);
|
||||||
Task<(Guid?, IDomainResult)> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts);
|
Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts);
|
||||||
Task<(List<string>?, IDomainResult)> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType);
|
Task<Result<List<string>?>> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType);
|
||||||
Task<IDomainResult> GetOrderAsync(Guid sessionId, string[] hostnames);
|
Task<Result> GetOrderAsync(Guid sessionId, string[] hostnames);
|
||||||
Task<IDomainResult> GetCertificatesAsync(Guid sessionId, string[] hostnames);
|
Task<Result> GetCertificatesAsync(Guid sessionId, string[] hostnames);
|
||||||
Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, string[] hostnames);
|
Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid sessionId, string[] hostnames);
|
||||||
Task<IDomainResult> RevokeCertificatesAsync(Guid sessionId, string[] hostnames);
|
Task<Result> RevokeCertificatesAsync(Guid sessionId, string[] hostnames);
|
||||||
Task<(Guid?, IDomainResult)> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[] hostnames);
|
Task<Result<Guid?>> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[] hostnames);
|
||||||
Task<IDomainResult> FullRevocationFlow(bool isStaging, Guid accountId, string description, string[] contacts, string[] hostnames);
|
Task<Result> FullRevocationFlow(bool isStaging, Guid accountId, string description, string[] contacts, string[] hostnames);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ICertsRestService : ICertsCommonService {
|
public interface ICertsRestService : ICertsCommonService {
|
||||||
Task<(Guid?, IDomainResult)> ConfigureClientAsync(ConfigureClientRequest requestData);
|
Task<Result<Guid?>> ConfigureClientAsync(ConfigureClientRequest requestData);
|
||||||
Task<(Guid?, IDomainResult)> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData);
|
Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData);
|
||||||
Task<(List<string>?, IDomainResult)> NewOrderAsync(Guid sessionId, NewOrderRequest requestData);
|
Task<Result<List<string>?>> NewOrderAsync(Guid sessionId, NewOrderRequest requestData);
|
||||||
Task<IDomainResult> GetOrderAsync(Guid sessionId, GetOrderRequest requestData);
|
Task<Result> GetOrderAsync(Guid sessionId, GetOrderRequest requestData);
|
||||||
Task<IDomainResult> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
|
Task<Result> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
|
||||||
Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
|
Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
|
||||||
Task<IDomainResult> RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData);
|
Task<Result> RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ICertsRestChallengeService {
|
public interface ICertsRestChallengeService {
|
||||||
(string?, IDomainResult) AcmeChallenge(string fileName);
|
Result<string?> AcmeChallenge(string fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ICertsFlowService
|
public interface ICertsFlowService
|
||||||
@ -50,13 +45,11 @@ public interface ICertsFlowService
|
|||||||
ICertsRestChallengeService { }
|
ICertsRestChallengeService { }
|
||||||
|
|
||||||
public class CertsFlowService : ICertsFlowService {
|
public class CertsFlowService : ICertsFlowService {
|
||||||
|
|
||||||
private readonly Configuration _appSettings;
|
private readonly Configuration _appSettings;
|
||||||
private readonly ILogger<CertsFlowService> _logger;
|
private readonly ILogger<CertsFlowService> _logger;
|
||||||
private readonly ILetsEncryptService _letsEncryptService;
|
private readonly ILetsEncryptService _letsEncryptService;
|
||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
private readonly IAgentService _agentService;
|
private readonly IAgentService _agentService;
|
||||||
|
|
||||||
private readonly string _acmePath;
|
private readonly string _acmePath;
|
||||||
|
|
||||||
public CertsFlowService(
|
public CertsFlowService(
|
||||||
@ -71,256 +64,213 @@ public class CertsFlowService : ICertsFlowService {
|
|||||||
_letsEncryptService = letsEncryptService;
|
_letsEncryptService = letsEncryptService;
|
||||||
_cacheService = cashService;
|
_cacheService = cashService;
|
||||||
_agentService = agentService;
|
_agentService = agentService;
|
||||||
|
|
||||||
_acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme");
|
_acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme");
|
||||||
if (!Directory.Exists(_acmePath))
|
if (!Directory.Exists(_acmePath))
|
||||||
Directory.CreateDirectory(_acmePath);
|
Directory.CreateDirectory(_acmePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Common methods
|
#region Common methods
|
||||||
|
public Result<string?> GetTermsOfService(Guid sessionId) {
|
||||||
|
var result = _letsEncryptService.GetTermsOfServiceUri(sessionId);
|
||||||
|
return result;
|
||||||
public (string?, IDomainResult) GetTermsOfService(Guid sessionId) {
|
|
||||||
var (terms, getTermsResult) = _letsEncryptService.GetTermsOfServiceUri(sessionId);
|
|
||||||
if (!getTermsResult.IsSuccess || terms == null)
|
|
||||||
return (null, getTermsResult);
|
|
||||||
|
|
||||||
return IDomainResult.Success<string>(terms);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDomainResult> CompleteChallengesAsync(Guid sessionId) {
|
public async Task<Result> CompleteChallengesAsync(Guid sessionId) {
|
||||||
return await _letsEncryptService.CompleteChallenges(sessionId);
|
return await _letsEncryptService.CompleteChallenges(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Internal methods
|
#region Internal methods
|
||||||
public async Task<(Guid?, IDomainResult)> ConfigureClientAsync(bool isStaging) {
|
public async Task<Result<Guid?>> ConfigureClientAsync(bool isStaging) {
|
||||||
var sessionId = Guid.NewGuid();
|
var sessionId = Guid.NewGuid();
|
||||||
|
|
||||||
var result = await _letsEncryptService.ConfigureClient(sessionId, isStaging);
|
var result = await _letsEncryptService.ConfigureClient(sessionId, isStaging);
|
||||||
if (!result.IsSuccess)
|
if (!result.IsSuccess)
|
||||||
return (null, result);
|
return result.ToResultOfType<Guid?>(default);
|
||||||
|
return Result<Guid?>.Ok(sessionId);
|
||||||
return IDomainResult.Success(sessionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(Guid?, IDomainResult)> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts) {
|
public async Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts) {
|
||||||
RegistrationCache? cache = null;
|
RegistrationCache? cache = null;
|
||||||
|
|
||||||
if (accountId == null) {
|
if (accountId == null) {
|
||||||
accountId = Guid.NewGuid();
|
accountId = Guid.NewGuid();
|
||||||
}
|
} else {
|
||||||
else {
|
var cacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId.Value);
|
||||||
var (loadedCache, loadCaceResutl) = await _cacheService.LoadAccountFromCacheAsync(accountId.Value);
|
if (!cacheResult.IsSuccess || cacheResult.Value == null) {
|
||||||
if (!loadCaceResutl.IsSuccess || loadCaceResutl == null) {
|
|
||||||
accountId = Guid.NewGuid();
|
accountId = Guid.NewGuid();
|
||||||
}
|
} else {
|
||||||
else {
|
cache = cacheResult.Value;
|
||||||
cache = loadedCache;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _letsEncryptService.Init(sessionId, accountId.Value, description, contacts, cache);
|
var result = await _letsEncryptService.Init(sessionId, accountId.Value, description, contacts, cache);
|
||||||
return result.IsSuccess ? IDomainResult.Success<Guid>(accountId.Value) : (null, result);
|
if (!result.IsSuccess)
|
||||||
|
return result.ToResultOfType<Guid?>(default);
|
||||||
|
return Result<Guid?>.Ok(accountId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(List<string>?, IDomainResult)> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType) {
|
public async Task<Result<List<string>?>> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType) {
|
||||||
var (results, newOrderResult) = await _letsEncryptService.NewOrder(sessionId, hostnames, challengeType);
|
var orderResult = await _letsEncryptService.NewOrder(sessionId, hostnames, challengeType);
|
||||||
if (!newOrderResult.IsSuccess || results == null)
|
if (!orderResult.IsSuccess || orderResult.Value == null)
|
||||||
return (null, newOrderResult);
|
return orderResult.ToResultOfType<List<string>?>(_ => null);
|
||||||
|
|
||||||
var challenges = new List<string>();
|
var challenges = new List<string>();
|
||||||
foreach (var result in results) {
|
foreach (var kvp in orderResult.Value) {
|
||||||
string[] splitToken = result.Value.Split('.');
|
string[] splitToken = kvp.Value.Split('.');
|
||||||
File.WriteAllText(Path.Combine(_acmePath, splitToken[0]), result.Value);
|
File.WriteAllText(Path.Combine(_acmePath, splitToken[0]), kvp.Value);
|
||||||
challenges.Add(splitToken[0]);
|
challenges.Add(splitToken[0]);
|
||||||
}
|
}
|
||||||
|
return Result<List<string>?>.Ok(challenges);
|
||||||
return IDomainResult.Success(challenges);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDomainResult> GetCertificatesAsync(Guid sessionId, string[] hostnames) {
|
public async Task<Result> GetCertificatesAsync(Guid sessionId, string[] hostnames) {
|
||||||
foreach (var subject in hostnames) {
|
foreach (var subject in hostnames) {
|
||||||
var result = await _letsEncryptService.GetCertificate(sessionId, subject);
|
var result = await _letsEncryptService.GetCertificate(sessionId, subject);
|
||||||
if (!result.IsSuccess)
|
if (!result.IsSuccess)
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
Thread.Sleep(1000);
|
Thread.Sleep(1000);
|
||||||
}
|
}
|
||||||
|
var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId);
|
||||||
// TODO: Move to separate method
|
if (!cacheResult.IsSuccess || cacheResult.Value == null)
|
||||||
// Persist the cache
|
return cacheResult;
|
||||||
var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId);
|
var saveResult = await _cacheService.SaveToCacheAsync(cacheResult.Value.AccountId, cacheResult.Value);
|
||||||
if (!getCacheResult.IsSuccess || cache == null)
|
|
||||||
return getCacheResult;
|
|
||||||
|
|
||||||
var saveResult = await _cacheService.SaveToCacheAsync(cache.AccountId, cache);
|
|
||||||
if (!saveResult.IsSuccess)
|
if (!saveResult.IsSuccess)
|
||||||
return saveResult;
|
return saveResult;
|
||||||
|
return Result.Ok();
|
||||||
return IDomainResult.Success();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDomainResult> GetOrderAsync(Guid sessionId, string[] hostnames) {
|
public async Task<Result> GetOrderAsync(Guid sessionId, string[] hostnames) {
|
||||||
return await _letsEncryptService.GetOrder(sessionId, hostnames);
|
return await _letsEncryptService.GetOrder(sessionId, hostnames);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, string[] hostnames) {
|
public async Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid sessionId, string[] hostnames) {
|
||||||
|
var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId);
|
||||||
var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId);
|
if (!cacheResult.IsSuccess || cacheResult.Value?.CachedCerts == null)
|
||||||
if (!getCacheResult.IsSuccess || cache?.CachedCerts == null)
|
return cacheResult.ToResultOfType<Dictionary<string, string>?>(_ => null);
|
||||||
return (null, getCacheResult);
|
|
||||||
|
|
||||||
|
|
||||||
var results = new Dictionary<string, string>();
|
var results = new Dictionary<string, string>();
|
||||||
foreach (var hostname in hostnames) {
|
foreach (var hostname in hostnames) {
|
||||||
CertificateCache? cert;
|
CertificateCache? cert;
|
||||||
if (cache.TryGetCachedCertificate(hostname, out cert)) {
|
if (cacheResult.Value.TryGetCachedCertificate(hostname, out cert)) {
|
||||||
var content = $"{cert.Cert}\n{cert.PrivatePem}";
|
var content = $"{cert.Cert}\n{cert.PrivatePem}";
|
||||||
results.Add(hostname, content);
|
results.Add(hostname, content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the certificates to the via agent
|
|
||||||
var uploadResult = await _agentService.UploadCerts(results);
|
var uploadResult = await _agentService.UploadCerts(results);
|
||||||
if (!uploadResult.IsSuccess)
|
if (!uploadResult.IsSuccess)
|
||||||
return (null, uploadResult);
|
return uploadResult.ToResultOfType<Dictionary<string, string>?>(default);
|
||||||
|
|
||||||
var reloadResult = await _agentService.ReloadService(_appSettings.Agent.ServiceToReload);
|
var reloadResult = await _agentService.ReloadService(_appSettings.Agent.ServiceToReload);
|
||||||
if (!reloadResult.IsSuccess)
|
if (!reloadResult.IsSuccess)
|
||||||
return (null, reloadResult);
|
return reloadResult.ToResultOfType<Dictionary<string, string>?>(default);
|
||||||
|
return Result<Dictionary<string, string>?>.Ok(results);
|
||||||
return IDomainResult.Success(results);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDomainResult> RevokeCertificatesAsync(Guid sessionId, string[] hostnames) {
|
public async Task<Result> RevokeCertificatesAsync(Guid sessionId, string[] hostnames) {
|
||||||
foreach (var hostname in hostnames) {
|
foreach (var hostname in hostnames) {
|
||||||
var result = await _letsEncryptService.RevokeCertificate(sessionId, hostname, RevokeReason.Unspecified);
|
var result = await _letsEncryptService.RevokeCertificate(sessionId, hostname, RevokeReason.Unspecified);
|
||||||
if (!result.IsSuccess)
|
if (!result.IsSuccess)
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId);
|
||||||
// TODO: Move to separate method
|
if (!cacheResult.IsSuccess || cacheResult.Value == null)
|
||||||
// Persist the cache
|
return cacheResult;
|
||||||
var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId);
|
var saveResult = await _cacheService.SaveToCacheAsync(cacheResult.Value.AccountId, cacheResult.Value);
|
||||||
if (!getCacheResult.IsSuccess || cache == null)
|
|
||||||
return getCacheResult;
|
|
||||||
|
|
||||||
var saveResult = await _cacheService.SaveToCacheAsync(cache.AccountId, cache);
|
|
||||||
if (!saveResult.IsSuccess)
|
if (!saveResult.IsSuccess)
|
||||||
return saveResult;
|
return saveResult;
|
||||||
|
return Result.Ok();
|
||||||
return IDomainResult.Success();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(Guid?, IDomainResult)> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[]hostnames) {
|
public async Task<Result<Guid?>> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[] hostnames) {
|
||||||
var (sessionId, configureClientResult) = await ConfigureClientAsync(isStaging);
|
var sessionResult = await ConfigureClientAsync(isStaging);
|
||||||
if (!configureClientResult.IsSuccess || sessionId == null)
|
if (!sessionResult.IsSuccess || sessionResult.Value == null)
|
||||||
return (null, configureClientResult);
|
return sessionResult;
|
||||||
|
|
||||||
(accountId, var initResult) = await InitAsync(sessionId.Value, accountId, description, contacts);
|
var sessionId = sessionResult.Value.Value;
|
||||||
|
|
||||||
|
var initResult = await InitAsync(sessionId, accountId, description, contacts);
|
||||||
if (!initResult.IsSuccess)
|
if (!initResult.IsSuccess)
|
||||||
return (null, initResult);
|
return initResult.ToResultOfType<Guid?>(_ => null);
|
||||||
|
|
||||||
var (challenges, newOrderResult) = await NewOrderAsync(sessionId.Value, hostnames, challengeType);
|
var challengesResult = await NewOrderAsync(sessionId, hostnames, challengeType);
|
||||||
if (!newOrderResult.IsSuccess)
|
if (!challengesResult.IsSuccess)
|
||||||
return (null, newOrderResult);
|
return challengesResult.ToResultOfType<Guid?>(_ => null);
|
||||||
|
|
||||||
if (challenges?.Count > 0) {
|
if (challengesResult.Value?.Count > 0) {
|
||||||
var challengeResult = await CompleteChallengesAsync(sessionId.Value);
|
var challengeResult = await CompleteChallengesAsync(sessionId);
|
||||||
if (!challengeResult.IsSuccess)
|
if (!challengeResult.IsSuccess)
|
||||||
return (null, challengeResult);
|
return challengeResult.ToResultOfType<Guid?>(default);
|
||||||
}
|
}
|
||||||
|
var getOrderResult = await GetOrderAsync(sessionId, hostnames);
|
||||||
|
|
||||||
var getOrderResult = await GetOrderAsync(sessionId.Value, hostnames);
|
|
||||||
if (!getOrderResult.IsSuccess)
|
if (!getOrderResult.IsSuccess)
|
||||||
return (null, getOrderResult);
|
return getOrderResult.ToResultOfType<Guid?>(default);
|
||||||
|
|
||||||
var certs = await GetCertificatesAsync(sessionId.Value, hostnames);
|
var certsResult = await GetCertificatesAsync(sessionId, hostnames);
|
||||||
if (!certs.IsSuccess)
|
if (!certsResult.IsSuccess)
|
||||||
return (null, certs);
|
return certsResult.ToResultOfType<Guid?>(default);
|
||||||
|
|
||||||
var (_, applyCertsResult) = await ApplyCertificatesAsync(sessionId.Value, hostnames);
|
// Bypass applying certificates in staging mode
|
||||||
|
if (!isStaging) {
|
||||||
|
var applyCertsResult = await ApplyCertificatesAsync(sessionId, hostnames);
|
||||||
if (!applyCertsResult.IsSuccess)
|
if (!applyCertsResult.IsSuccess)
|
||||||
return (null, applyCertsResult);
|
return applyCertsResult.ToResultOfType<Guid?>(_ => null);
|
||||||
|
|
||||||
return IDomainResult.Success(accountId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDomainResult> FullRevocationFlow(bool isStaging, Guid accountId, string description, string[] contacts, string[] hostnames) {
|
return Result<Guid?>.Ok(initResult.Value);
|
||||||
var (sessionId, configureClientResult) = await ConfigureClientAsync(isStaging);
|
}
|
||||||
if (!configureClientResult.IsSuccess || sessionId == null)
|
|
||||||
return configureClientResult;
|
|
||||||
|
|
||||||
var (_, initResult) = await InitAsync(sessionId.Value, accountId, description, contacts);
|
public async Task<Result> 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)
|
if (!initResult.IsSuccess)
|
||||||
return initResult;
|
return initResult;
|
||||||
|
var revokeResult = await RevokeCertificatesAsync(sessionId, hostnames);
|
||||||
var revokeResult = await RevokeCertificatesAsync(sessionId.Value, hostnames);
|
|
||||||
if (!revokeResult.IsSuccess)
|
if (!revokeResult.IsSuccess)
|
||||||
return revokeResult;
|
return revokeResult;
|
||||||
|
return Result.Ok();
|
||||||
return IDomainResult.Success();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region REST methods
|
#region REST methods
|
||||||
|
public async Task<Result<Guid?>> ConfigureClientAsync(ConfigureClientRequest requestData) {
|
||||||
|
return await ConfigureClientAsync(requestData.IsStaging);
|
||||||
|
}
|
||||||
|
public async Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData) {
|
||||||
|
return await InitAsync(sessionId, accountId, requestData.Description, requestData.Contacts);
|
||||||
|
}
|
||||||
|
public async Task<Result<List<string>>> NewOrderAsync(Guid sessionId, NewOrderRequest requestData) {
|
||||||
|
return await NewOrderAsync(sessionId, requestData.Hostnames, requestData.ChallengeType);
|
||||||
|
}
|
||||||
|
public async Task<Result> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) {
|
||||||
|
return await GetCertificatesAsync(sessionId, requestData.Hostnames);
|
||||||
|
}
|
||||||
|
public async Task<Result> GetOrderAsync(Guid sessionId, GetOrderRequest requestData) {
|
||||||
|
return await GetOrderAsync(sessionId, requestData.Hostnames);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<(Guid?, IDomainResult)> ConfigureClientAsync(ConfigureClientRequest requestData) =>
|
public async Task<Result<Dictionary<string, string>>> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) =>
|
||||||
ConfigureClientAsync(requestData.IsStaging);
|
await ApplyCertificatesAsync(sessionId, requestData.Hostnames);
|
||||||
|
public async Task<Result> RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData) =>
|
||||||
public Task<(Guid?, IDomainResult)> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData) =>
|
await RevokeCertificatesAsync(sessionId, requestData.Hostnames);
|
||||||
InitAsync(sessionId, accountId, requestData.Description, requestData.Contacts);
|
|
||||||
|
|
||||||
public Task<(List<string>?, IDomainResult)> NewOrderAsync(Guid sessionId, NewOrderRequest requestData) =>
|
|
||||||
NewOrderAsync(sessionId, requestData.Hostnames, requestData.ChallengeType);
|
|
||||||
|
|
||||||
public Task<IDomainResult> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) =>
|
|
||||||
GetCertificatesAsync(sessionId, requestData.Hostnames);
|
|
||||||
|
|
||||||
public Task<IDomainResult> GetOrderAsync(Guid sessionId, GetOrderRequest requestData) =>
|
|
||||||
GetOrderAsync(sessionId, requestData.Hostnames);
|
|
||||||
|
|
||||||
public Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) =>
|
|
||||||
ApplyCertificatesAsync(sessionId, requestData.Hostnames);
|
|
||||||
|
|
||||||
public Task<IDomainResult> RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData) =>
|
|
||||||
RevokeCertificatesAsync(sessionId, requestData.Hostnames);
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Acme Challenge REST methods
|
#region Acme Challenge REST methods
|
||||||
|
public Result<string?> AcmeChallenge(string fileName) {
|
||||||
public (string?, IDomainResult) AcmeChallenge(string fileName) {
|
|
||||||
DeleteExporedChallenges();
|
DeleteExporedChallenges();
|
||||||
|
|
||||||
var challengePath = Path.Combine(_acmePath, fileName);
|
var challengePath = Path.Combine(_acmePath, fileName);
|
||||||
if(!File.Exists(challengePath))
|
if(!File.Exists(challengePath))
|
||||||
return IDomainResult.NotFound<string?>();
|
return Result<string?>.NotFound(null);
|
||||||
|
|
||||||
var fileContent = File.ReadAllText(Path.Combine(_acmePath, fileName));
|
var fileContent = File.ReadAllText(Path.Combine(_acmePath, fileName));
|
||||||
return IDomainResult.Success(fileContent);
|
return Result<string?>.Ok(fileContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeleteExporedChallenges() {
|
private void DeleteExporedChallenges() {
|
||||||
var currentDate = DateTime.Now;
|
var currentDate = DateTime.Now;
|
||||||
|
|
||||||
foreach (var file in Directory.GetFiles(_acmePath)) {
|
foreach (var file in Directory.GetFiles(_acmePath)) {
|
||||||
try {
|
try {
|
||||||
var creationTime = File.GetCreationTime(file);
|
var creationTime = File.GetCreationTime(file);
|
||||||
|
|
||||||
// Calculate the time difference
|
|
||||||
var timeDifference = currentDate - creationTime;
|
var timeDifference = currentDate - creationTime;
|
||||||
|
|
||||||
// If the file is older than 1 day, delete it
|
|
||||||
if (timeDifference.TotalDays > 1) {
|
if (timeDifference.TotalDays > 1) {
|
||||||
|
|
||||||
|
|
||||||
File.Delete(file);
|
File.Delete(file);
|
||||||
_logger.LogInformation($"Deleted file: {file}");
|
_logger.LogInformation($"Deleted file: {file}");
|
||||||
}
|
}
|
||||||
@ -330,6 +280,5 @@ public class CertsFlowService : ICertsFlowService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user