(refactor): domain results

This commit is contained in:
Maksym Sadovnychyy 2023-08-06 22:35:13 +02:00
parent f7411a4e3d
commit 327f3be2c7
5 changed files with 619 additions and 444 deletions

View File

@ -0,0 +1,10 @@
namespace MaksIT.LetsEncrypt.Entities {
public class SendResult<TResult> {
public TResult? Result { get; set; }
public string? ResponseText { get; set; }
}
}

View File

@ -1,23 +1,19 @@
 using MaksIT.Core.Extensions;
using MaksIT.LetsEncrypt.Models.Responses;
namespace MaksIT.LetsEncrypt.Exceptions; namespace MaksIT.LetsEncrypt.Exceptions;
public class LetsEncrytException : Exception { public class LetsEncrytException : Exception {
public LetsEncrytException(Problem problem, HttpResponseMessage response)
: base($"{problem.Type}: {problem.Detail}") { public Problem? Problem { get; }
public HttpResponseMessage Response { get; }
public LetsEncrytException(
Problem? problem,
HttpResponseMessage response
) : base(problem != null ? $"{problem.Type}: {problem.Detail}" : "") {
Problem = problem; Problem = problem;
Response = response; Response = response;
} }
public Problem Problem { get; }
public HttpResponseMessage Response { get; }
} }
public class Problem {
public string Type { get; set; }
public string Detail { get; set; }
public string RawJson { get; set; }
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.LetsEncrypt.Models.Responses {
public class Problem {
public string Type { get; set; }
public string Detail { get; set; }
public string RawJson { get; set; }
}
}

View File

@ -18,24 +18,25 @@ using MaksIT.LetsEncrypt.Models.Responses;
using MaksIT.LetsEncrypt.Models.Interfaces; using MaksIT.LetsEncrypt.Models.Interfaces;
using MaksIT.LetsEncrypt.Models.Requests; using MaksIT.LetsEncrypt.Models.Requests;
using MaksIT.LetsEncrypt.Entities.Jws; using MaksIT.LetsEncrypt.Entities.Jws;
using DomainResults.Common;
namespace MaksIT.LetsEncrypt.Services; namespace MaksIT.LetsEncrypt.Services;
public interface ILetsEncryptService { public interface ILetsEncryptService {
Task ConfigureClient(string url); Task<IDomainResult> ConfigureClient(string url);
Task Init(string[] contacts, RegistrationCache? registrationCache); Task<IDomainResult> Init(string[] contacts, RegistrationCache? registrationCache);
string GetTermsOfServiceUri();
Task<Dictionary<string, string>> NewOrder(string[] hostnames, string challengeType);
Task CompleteChallenges();
Task GetOrder(string[] hostnames);
Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject);
RegistrationCache? GetRegistrationCache(); RegistrationCache? GetRegistrationCache();
(string?, IDomainResult) GetTermsOfServiceUri();
Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(string[] hostnames, string challengeType);
Task<IDomainResult> CompleteChallenges();
Task<IDomainResult> GetOrder(string[] hostnames);
Task<((X509Certificate2 Cert, RSA PrivateKey)?, IDomainResult)> GetCertificate(string subject);
} }
@ -76,11 +77,22 @@ public class LetsEncryptService : ILetsEncryptService {
/// <param name="url"></param> /// <param name="url"></param>
/// <param name="contacts"></param> /// <param name="contacts"></param>
/// <returns></returns> /// <returns></returns>
public async Task ConfigureClient(string url) { public async Task<IDomainResult> ConfigureClient(string url) {
try {
_httpClient.BaseAddress ??= new Uri(url); _httpClient.BaseAddress ??= new Uri(url);
(_directory, _) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null); var (directory, getAcmeDirectoryResult) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null);
if (!getAcmeDirectoryResult.IsSuccess)
return getAcmeDirectoryResult;
_directory = directory.Result;
return IDomainResult.Success();
}
catch (Exception ex) {
_logger.LogError(ex, "Let's Encrypt client unhandled exception");
return IDomainResult.CriticalDependencyError();
}
} }
/// <summary> /// <summary>
@ -89,13 +101,17 @@ public class LetsEncryptService : ILetsEncryptService {
/// <param name="contacts"></param> /// <param name="contacts"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
public async Task Init(string? [] contacts, RegistrationCache? cache) { public async Task<IDomainResult> Init(string? [] contacts, RegistrationCache? cache) {
try {
_logger.LogInformation($"Executing {nameof(Init)}...");
if (contacts == null || contacts.Length == 0) if (contacts == null || contacts.Length == 0)
throw new ArgumentNullException(); return IDomainResult.Failed();
if (_directory == null) if (_directory == null)
throw new ArgumentNullException(); return IDomainResult.Failed();
var accountKey = new RSACryptoServiceProvider(4096); var accountKey = new RSACryptoServiceProvider(4096);
@ -113,18 +129,29 @@ public class LetsEncryptService : ILetsEncryptService {
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray() Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
}; };
var (account, response) = await SendAsync<Account>(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder); var (account, postAccuntResult) = await SendAsync<Account>(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder);
_jwsService.SetKeyId(account.Location.ToString()); _jwsService.SetKeyId(account.Result.Location.ToString());
if (account.Status != "valid") if (account.Result.Status != "valid") {
throw new InvalidOperationException($"Account status is not valid, was: {account.Status} \r\n {response}"); _logger.LogError($"Account status is not valid, was: {account.Result.Status} \r\n {account.ResponseText}");
return IDomainResult.Failed();
}
_cache = new RegistrationCache { _cache = new RegistrationCache {
Location = account.Location, Location = account.Result.Location,
AccountKey = accountKey.ExportCspBlob(true), AccountKey = accountKey.ExportCspBlob(true),
Id = account.Id, Id = account.Result.Id,
Key = account.Key Key = account.Result.Key
}; };
return IDomainResult.Success();
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
} }
/// <summary> /// <summary>
@ -139,15 +166,24 @@ public class LetsEncryptService : ILetsEncryptService {
/// </summary> /// </summary>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
public string GetTermsOfServiceUri() { public (string?, IDomainResult) GetTermsOfServiceUri() {
try {
if (_directory == null) _logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}...");
throw new NullReferenceException();
return _directory.Meta.TermsOfService; if (_directory == null) {
return IDomainResult.Failed<string?>();
} }
return IDomainResult.Success(_directory.Meta.TermsOfService);
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError<string?>(message);
}
}
/// <summary> /// <summary>
/// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange. /// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange.
@ -164,7 +200,11 @@ public class LetsEncryptService : ILetsEncryptService {
/// <param name="challengeType"></param> /// <param name="challengeType"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
public async Task<Dictionary<string, string>> NewOrder(string[] hostnames, string challengeType) { public async Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(string[] hostnames, string challengeType) {
try {
_logger.LogInformation($"Executing {nameof(NewOrder)}...");
_challenges.Clear(); _challenges.Clear();
var letsEncryptOrder = new Order { var letsEncryptOrder = new Order {
@ -175,27 +215,38 @@ public class LetsEncryptService : ILetsEncryptService {
}).ToArray() }).ToArray()
}; };
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); var (order, postNewOrderResult) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
if (!postNewOrderResult.IsSuccess) {
return (null, postNewOrderResult);
}
if (order.Status == "ready") if (order.Result.Status == "ready")
return new Dictionary<string, string>(); return IDomainResult.Success(new Dictionary<string, string>());
if (order.Status != "pending") if (order.Result.Status != "pending") {
throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}"); _logger.LogError($"Created new order and expected status 'pending', but got: {order.Result.Status} \r\n {order.Result}");
return IDomainResult.Failed<Dictionary<string, string>?>();
}
_currentOrder = order; _currentOrder = order.Result;
var results = new Dictionary<string, string>(); var results = new Dictionary<string, string>();
foreach (var item in order.Authorizations) { foreach (var item in order.Result.Authorizations) {
var (challengeResponse, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, true, null);
if (challengeResponse.Status == "valid") var (challengeResponse, postAuthorisationChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, true, null);
if (!postAuthorisationChallengeResult.IsSuccess) {
return (null, postAuthorisationChallengeResult);
}
if (challengeResponse.Result.Status == "valid")
continue; continue;
if (challengeResponse.Status != "pending") if (challengeResponse.Result.Status != "pending") {
throw new InvalidOperationException($"Expected autorization status 'pending', but got: {order.Status} \r\n {responseText}"); _logger.LogError($"Expected autorization status 'pending', but got: {order.Result.Status} \r\n {challengeResponse.ResponseText}");
return IDomainResult.Failed<Dictionary<string, string>?>();
}
var challenge = challengeResponse.Challenges.First(x => x.Type == challengeType); var challenge = challengeResponse.Result.Challenges.First(x => x.Type == challengeType);
_challenges.Add(challenge); _challenges.Add(challenge);
var keyToken = _jwsService.GetKeyAuthorization(challenge.Token); var keyToken = _jwsService.GetKeyAuthorization(challenge.Token);
@ -213,7 +264,7 @@ public class LetsEncryptService : ILetsEncryptService {
case "dns-01": { case "dns-01": {
using (var sha256 = SHA256.Create()) { using (var sha256 = SHA256.Create()) {
var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken))); var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
results[challengeResponse.Identifier.Value] = dnsToken; results[challengeResponse.Result.Identifier.Value] = dnsToken;
} }
break; break;
} }
@ -230,7 +281,7 @@ public class LetsEncryptService : ILetsEncryptService {
// representation of the key authorization. // representation of the key authorization.
case "http-01": { case "http-01": {
results[challengeResponse.Identifier.Value] = keyToken; results[challengeResponse.Result.Identifier.Value] = keyToken;
break; break;
} }
@ -239,7 +290,14 @@ public class LetsEncryptService : ILetsEncryptService {
} }
} }
return results; return IDomainResult.Success(results);
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError<Dictionary<string, string>?>(message);
}
} }
/// <summary> /// <summary>
@ -247,14 +305,23 @@ public class LetsEncryptService : ILetsEncryptService {
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
/// <exception cref="InvalidOperationException"></exception> /// <exception cref="InvalidOperationException"></exception>
public async Task CompleteChallenges() { public async Task<IDomainResult> CompleteChallenges() {
try {
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
if (_currentOrder?.Identifiers == null) {
return IDomainResult.Failed();
}
for (var index = 0; index < _challenges.Count; index++) { for (var index = 0; index < _challenges.Count; index++) {
var challenge = _challenges[index]; var challenge = _challenges[index];
var start = DateTime.UtcNow;
while (true) { while (true) {
AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge(); var authorizeChallenge = new AuthorizeChallenge();
switch (challenge.Type) { switch (challenge.Type) {
case "dns-01": { case "dns-01": {
@ -268,16 +335,34 @@ public class LetsEncryptService : ILetsEncryptService {
} }
} }
var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}"); var (authChallenge, postAuthChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}");
if (!postAuthChallengeResult.IsSuccess) {
return postAuthChallengeResult;
}
if (result.Status == "valid") if (authChallenge.Result.Status == "valid")
break; break;
if (result.Status != "pending")
throw new InvalidOperationException($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {responseText}"); if (authChallenge.Result.Status != "pending") {
_logger.LogError($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {authChallenge.ResponseText}");
return IDomainResult.Failed();
}
await Task.Delay(1000); await Task.Delay(1000);
if ((DateTime.UtcNow - start).Seconds > 120)
throw new TimeoutException();
} }
} }
return IDomainResult.Success();
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
} }
/// <summary> /// <summary>
@ -285,7 +370,11 @@ public class LetsEncryptService : ILetsEncryptService {
/// </summary> /// </summary>
/// <param name="hostnames"></param> /// <param name="hostnames"></param>
/// <returns></returns> /// <returns></returns>
public async Task GetOrder(string[] hostnames) { public async Task<IDomainResult> GetOrder(string[] hostnames) {
try {
_logger.LogInformation($"Executing {nameof(GetOrder)}");
var letsEncryptOrder = new Order { var letsEncryptOrder = new Order {
Expires = DateTime.UtcNow.AddDays(2), Expires = DateTime.UtcNow.AddDays(2),
@ -295,9 +384,20 @@ public class LetsEncryptService : ILetsEncryptService {
}).ToArray() }).ToArray()
}; };
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
if (!postOrderResult.IsSuccess)
return postOrderResult;
_currentOrder = order; _currentOrder = order.Result;
return IDomainResult.Success();
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
} }
/// <summary> /// <summary>
@ -306,13 +406,14 @@ public class LetsEncryptService : ILetsEncryptService {
/// <param name="subject"></param> /// <param name="subject"></param>
/// <returns>Cert and Private key</returns> /// <returns>Cert and Private key</returns>
/// <exception cref="InvalidOperationException"></exception> /// <exception cref="InvalidOperationException"></exception>
public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) { public async Task<((X509Certificate2 Cert, RSA PrivateKey)?, IDomainResult)> GetCertificate(string subject) {
_logger.LogInformation($"Invoked: {nameof(GetCertificate)}"); try {
_logger.LogInformation($"Executing {nameof(GetCertificate)}...");
if (_currentOrder == null) {
if (_currentOrder == null) return IDomainResult.Failed<(X509Certificate2 Cert, RSA PrivateKey)?>();
throw new ArgumentNullException(); }
var key = new RSACryptoServiceProvider(4096); var key = new RSACryptoServiceProvider(4096);
var csr = new CertificateRequest("CN=" + subject, var csr = new CertificateRequest("CN=" + subject,
@ -338,39 +439,54 @@ public class LetsEncryptService : ILetsEncryptService {
await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray()); await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray());
if (_currentOrder.Status == "ready") { if (_currentOrder.Status == "ready") {
var (response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder); var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder);
if (!postOrderResult.IsSuccess || order?.Result == null)
return (null, postOrderResult);
if (response.Status == "processing")
(response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Location, true, null);
if (response.Status == "valid") { if (order.Result.Status == "processing") {
certificateUrl = response.Certificate; (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Location, true, null);
if (!postOrderResult.IsSuccess || order?.Result == null)
return (null, postOrderResult);
}
if (order.Result.Status == "valid") {
certificateUrl = order.Result.Certificate;
} }
} }
if ((start - DateTime.UtcNow).Seconds > 120) if ((DateTime.UtcNow - start).Seconds > 120)
throw new TimeoutException(); throw new TimeoutException();
await Task.Delay(1000); await Task.Delay(1000);
continue;
// throw new InvalidOperationException(/*$"Invalid order status: "*/);
} }
var (pem, _) = await SendAsync<string>(HttpMethod.Post, certificateUrl, true, null); var (pem, postPemResult) = await SendAsync<string>(HttpMethod.Post, certificateUrl, true, null);
if (!postPemResult.IsSuccess || pem?.Result == null)
return (null, postPemResult);
if (_cache == null)
throw new NullReferenceException(); if (_cache == null) {
_logger.LogError($"{nameof(_cache)} is null");
return IDomainResult.Failed<(X509Certificate2 Cert, RSA PrivateKey)?>();
}
_cache.CachedCerts ??= new Dictionary<string, CertificateCache>(); _cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
_cache.CachedCerts[subject] = new CertificateCache { _cache.CachedCerts[subject] = new CertificateCache {
Cert = pem, Cert = pem.Result,
Private = key.ExportCspBlob(true) Private = key.ExportCspBlob(true)
}; };
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem)); var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem.Result));
return (cert, key); return IDomainResult.Success((cert, key));
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError< (X509Certificate2 Cert, RSA PrivateKey)?>(message);
}
} }
/// <summary> /// <summary>
@ -378,7 +494,7 @@ public class LetsEncryptService : ILetsEncryptService {
/// </summary> /// </summary>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
public Task KeyChange() { public Task<IDomainResult> KeyChange() {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -387,27 +503,71 @@ public class LetsEncryptService : ILetsEncryptService {
/// </summary> /// </summary>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
public Task RevokeCertificate() { public Task<IDomainResult> RevokeCertificate() {
throw new NotImplementedException(); throw new NotImplementedException();
} }
/// <summary>
/// Request New Nonce to be able to start POST requests
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
private async Task<(string?, IDomainResult)> NewNonce() {
try {
_logger.LogInformation($"Executing {nameof(NewNonce)}...");
if (_directory == null)
IDomainResult.Failed();
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce));
return IDomainResult.Success(result.Headers.GetValues("Replay-Nonce").First());
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError<string?>(message);
}
}
/// <summary> /// <summary>
/// Main method used to send data to LetsEncrypt /// Main method used to send data to LetsEncrypt
/// </summary> /// </summary>
/// <typeparam name="TResult"></typeparam> /// <typeparam name="TResult"></typeparam>
/// <param name="method"></param> /// <param name="method"></param>
/// <param name="uri"></param> /// <param name="uri"></param>
/// <param name="message"></param> /// <param name="requestModel"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
private async Task<(TResult, string)> SendAsync<TResult>(HttpMethod method, Uri uri, bool isPostAsGet, object? message) where TResult : class { private async Task<(SendResult<TResult>?, IDomainResult)> SendAsync<TResult>(HttpMethod method, Uri uri, bool isPostAsGet, object? requestModel) {
try {
_logger.LogInformation($"Executing {nameof(SendAsync)}...");
//if (_jwsService == null) {
// _logger.LogError($"{nameof(_jwsService)} is null");
// return IDomainResult.Failed<SendResult<TResult>?>();
//}
var request = new HttpRequestMessage(method, uri); var request = new HttpRequestMessage(method, uri);
_nonce = uri.OriginalString != "directory" if (uri.OriginalString != "directory") {
? await NewNonce() var (nonce, newNonceResult) = await NewNonce();
: default; if (!newNonceResult.IsSuccess || nonce == null) {
return (null, newNonceResult);
}
if (message != null || isPostAsGet) { _nonce = nonce;
}
else {
_nonce = default;
}
if (requestModel != null || isPostAsGet) {
var jwsHeader = new JwsHeader { var jwsHeader = new JwsHeader {
Url = uri, Url = uri,
}; };
@ -417,7 +577,7 @@ public class LetsEncryptService : ILetsEncryptService {
var encodedMessage = isPostAsGet var encodedMessage = isPostAsGet
? _jwsService.Encode(jwsHeader) ? _jwsService.Encode(jwsHeader)
: _jwsService.Encode(message, jwsHeader); : _jwsService.Encode(requestModel, jwsHeader);
var json = encodedMessage.ToJson(); var json = encodedMessage.ToJson();
@ -436,17 +596,15 @@ public class LetsEncryptService : ILetsEncryptService {
if (method == HttpMethod.Post) if (method == HttpMethod.Post)
_nonce = response.Headers.GetValues("Replay-Nonce").First(); _nonce = response.Headers.GetValues("Replay-Nonce").First();
if (response.Content.Headers.ContentType.MediaType == "application/problem+json") {
var problemJson = await response.Content.ReadAsStringAsync();
var problem = problemJson.ToObject<Problem>();
problem.RawJson = problemJson;
throw new LetsEncrytException(problem, response);
}
var responseText = await response.Content.ReadAsStringAsync(); var responseText = await response.Content.ReadAsStringAsync();
if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") { if (response.Content.Headers.ContentType?.MediaType == "application/problem+json")
return ((TResult)(object)responseText, null); throw new LetsEncrytException(responseText.ToObject<Problem>(), response);
if (response.Content.Headers.ContentType?.MediaType == "application/pem-certificate-chain" && typeof(TResult) == typeof(string)) {
return IDomainResult.Success(new SendResult<TResult> {
Result = (TResult)(object)responseText
});
} }
var responseContent = responseText.ToObject<TResult>(); var responseContent = responseText.ToObject<TResult>();
@ -456,19 +614,17 @@ public class LetsEncryptService : ILetsEncryptService {
ihl.Location = response.Headers.Location; ihl.Location = response.Headers.Location;
} }
return (responseContent, responseText); return IDomainResult.Success(new SendResult<TResult> {
Result = responseContent,
ResponseText = responseText
});
} }
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
/// <summary> _logger.LogError(ex, message);
/// Request New Nonce to be able to start POST requests return IDomainResult.CriticalDependencyError<SendResult<TResult>?>(message);
/// </summary> }
/// <param name="token"></param>
/// <returns></returns>
private async Task<string> NewNonce() {
if (_directory == null)
throw new NotImplementedException();
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce));
return result.Headers.GetValues("Replay-Nonce").First();
} }
} }

View File

@ -39,15 +39,16 @@ public class App : IApp {
public async Task Run(string[] args) { public async Task Run(string[] args) {
_logger.LogInformation("Letsencrypt client estarted..."); try {
_logger.LogInformation("Let's Encrypt client. Started...");
foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List<LetsEncryptEnvironment>()) { foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List<LetsEncryptEnvironment>()) {
try {
_logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}"); _logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}");
//loop all customers //loop all customers
foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List<Customer>()) { foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List<Customer>()) {
try {
_logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}"); _logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}");
//define cache folder //define cache folder
@ -66,7 +67,7 @@ public class App : IApp {
foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List<Site>()) { foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List<Site>()) {
_logger.LogInformation($"Managing site: {site.Name}"); _logger.LogInformation($"Managing site: {site.Name}");
try {
//create folder for ssl //create folder for ssl
string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name); string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name);
if (!Directory.Exists(sslPath)) { if (!Directory.Exists(sslPath)) {
@ -75,20 +76,22 @@ public class App : IApp {
var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json"); var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json");
//1. Client initialization
#region LetsEncrypt client configuration and local registration cache initialization
_logger.LogInformation("1. Client Initialization..."); _logger.LogInformation("1. Client Initialization...");
#region LetsEncrypt client configuration
await _letsEncryptService.ConfigureClient(env.Url); await _letsEncryptService.ConfigureClient(env.Url);
#endregion
#region LetsEncrypt local registration cache initialization
var registrationCache = (File.Exists(cacheFile) var registrationCache = (File.Exists(cacheFile)
? File.ReadAllText(cacheFile) ? File.ReadAllText(cacheFile)
: null) : null)
.ToObject<RegistrationCache>(); .ToObject<RegistrationCache>();
await _letsEncryptService.Init(customer.Contacts, registrationCache); var initResult = await _letsEncryptService.Init(customer.Contacts, registrationCache);
if (!initResult.IsSuccess) {
continue;
}
#endregion #endregion
#region LetsEncrypt terms of service #region LetsEncrypt terms of service
@ -110,13 +113,15 @@ public class App : IApp {
} }
else { else {
//try to make new order
try {
//create new orders
Console.WriteLine("2. Client New Order...");
//create new orders
#region LetsEncrypt new order #region LetsEncrypt new order
var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge); _logger.LogInformation("2. Client New Order...");
var (orders, newOrderResult) = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge);
if (!newOrderResult.IsSuccess || orders == null) {
continue;
}
#endregion #endregion
if (orders.Count > 0) { if (orders.Count > 0) {
@ -160,7 +165,10 @@ public class App : IApp {
#region LetsEncrypt complete challenges #region LetsEncrypt complete challenges
_logger.LogInformation("3. Client Complete Challange..."); _logger.LogInformation("3. Client Complete Challange...");
await _letsEncryptService.CompleteChallenges(); var completeChallengesResult = await _letsEncryptService.CompleteChallenges();
if (!completeChallengesResult.IsSuccess) {
continue;
}
_logger.LogInformation("Challanges comleted."); _logger.LogInformation("Challanges comleted.");
#endregion #endregion
@ -168,7 +176,13 @@ public class App : IApp {
#region Download new certificate #region Download new certificate
_logger.LogInformation("4. Download certificate..."); _logger.LogInformation("4. Download certificate...");
var (cert, key) = await _letsEncryptService.GetCertificate(site.Name); var (certData, getCertResult) = await _letsEncryptService.GetCertificate(site.Name);
if (!getCertResult.IsSuccess || certData == null) {
continue;
}
// not used in this scenario
// var (cert, key) = certData.Value;
#endregion #endregion
#region Persist cache #region Persist cache
@ -183,7 +197,7 @@ public class App : IApp {
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate); File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate);
if(certRes.PrivateKey != null) if (certRes.PrivateKey != null)
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem()); File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem());
_logger.LogInformation("Certificate saved."); _logger.LogInformation("Certificate saved.");
@ -203,37 +217,19 @@ public class App : IApp {
_logger.LogError("Unable to get new cached certificate."); _logger.LogError("Unable to get new cached certificate.");
} }
#endregion #endregion
}
}
}
}
_logger.LogInformation($"Let's Encrypt client. Execution complete.");
} }
catch (Exception ex) { catch (Exception ex) {
_logger.LogError(ex, ""); _logger.LogError(ex, $"Let's Encrypt client. Unhandled exception.");
await _letsEncryptService.GetOrder(site.Hosts);
}
} }
}
catch (Exception ex) {
_logger.LogError(ex, "Customer unhandled error");
}
}
}
catch (Exception ex) {
_logger.LogError(ex, "Environment unhandled error");
}
}
if (env.Name == "ProductionV2") {
_terminalService.Exec("systemctl restart nginx");
}
}
catch (Exception ex) {
_logger.LogError(ex.Message.ToString());
break;
}
}
} }
@ -244,7 +240,7 @@ public class App : IApp {
SSHClientSettings sshSettings, SSHClientSettings sshSettings,
string workDir, string workDir,
string fileName, string fileName,
byte [] bytes, byte[] bytes,
string owner, string owner,
string changeMode string changeMode
) { ) {
@ -260,5 +256,7 @@ public class App : IApp {
sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R"); sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R");
sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R"); sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R");
//sshService.RunSudoCommand(sshSettings.Password, $"systemctl restart nginx");
} }
} }