mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(refactor): domain results
This commit is contained in:
parent
f7411a4e3d
commit
327f3be2c7
10
src/LetsEncrypt/Entities/LetsEncrypt/SendResult.cs
Normal file
10
src/LetsEncrypt/Entities/LetsEncrypt/SendResult.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace MaksIT.LetsEncrypt.Entities {
|
||||||
|
public class SendResult<TResult> {
|
||||||
|
|
||||||
|
public TResult? Result { get; set; }
|
||||||
|
|
||||||
|
public string? ResponseText { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
15
src/LetsEncrypt/Models/Responses/Problem.cs
Normal file
15
src/LetsEncrypt/Models/Responses/Problem.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
var (directory, getAcmeDirectoryResult) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null);
|
||||||
|
if (!getAcmeDirectoryResult.IsSuccess)
|
||||||
|
return getAcmeDirectoryResult;
|
||||||
|
|
||||||
(_directory, _) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null);
|
_directory = directory.Result;
|
||||||
|
|
||||||
|
return IDomainResult.Success();
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
_logger.LogError(ex, "Let's Encrypt client unhandled exception");
|
||||||
|
return IDomainResult.CriticalDependencyError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -89,42 +101,57 @@ 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) {
|
||||||
|
|
||||||
if (contacts == null || contacts.Length == 0)
|
try {
|
||||||
throw new ArgumentNullException();
|
|
||||||
|
|
||||||
if (_directory == null)
|
_logger.LogInformation($"Executing {nameof(Init)}...");
|
||||||
throw new ArgumentNullException();
|
|
||||||
|
|
||||||
var accountKey = new RSACryptoServiceProvider(4096);
|
if (contacts == null || contacts.Length == 0)
|
||||||
|
return IDomainResult.Failed();
|
||||||
|
|
||||||
if (cache != null && cache.AccountKey != null) {
|
if (_directory == null)
|
||||||
_cache = cache;
|
return IDomainResult.Failed();
|
||||||
accountKey.ImportCspBlob(cache.AccountKey);
|
|
||||||
|
var accountKey = new RSACryptoServiceProvider(4096);
|
||||||
|
|
||||||
|
if (cache != null && cache.AccountKey != null) {
|
||||||
|
_cache = cache;
|
||||||
|
accountKey.ImportCspBlob(cache.AccountKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Account request
|
||||||
|
_jwsService = new JwsService(accountKey);
|
||||||
|
|
||||||
|
|
||||||
|
var letsEncryptOrder = new Account {
|
||||||
|
TermsOfServiceAgreed = true,
|
||||||
|
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
var (account, postAccuntResult) = await SendAsync<Account>(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder);
|
||||||
|
_jwsService.SetKeyId(account.Result.Location.ToString());
|
||||||
|
|
||||||
|
if (account.Result.Status != "valid") {
|
||||||
|
_logger.LogError($"Account status is not valid, was: {account.Result.Status} \r\n {account.ResponseText}");
|
||||||
|
return IDomainResult.Failed();
|
||||||
|
}
|
||||||
|
|
||||||
|
_cache = new RegistrationCache {
|
||||||
|
Location = account.Result.Location,
|
||||||
|
AccountKey = accountKey.ExportCspBlob(true),
|
||||||
|
Id = account.Result.Id,
|
||||||
|
Key = account.Result.Key
|
||||||
|
};
|
||||||
|
|
||||||
|
return IDomainResult.Success();
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
// New Account request
|
_logger.LogError(ex, message);
|
||||||
_jwsService = new JwsService(accountKey);
|
return IDomainResult.CriticalDependencyError(message);
|
||||||
|
}
|
||||||
|
|
||||||
var letsEncryptOrder = new Account {
|
|
||||||
TermsOfServiceAgreed = true,
|
|
||||||
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
|
|
||||||
};
|
|
||||||
|
|
||||||
var (account, response) = await SendAsync<Account>(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder);
|
|
||||||
_jwsService.SetKeyId(account.Location.ToString());
|
|
||||||
|
|
||||||
if (account.Status != "valid")
|
|
||||||
throw new InvalidOperationException($"Account status is not valid, was: {account.Status} \r\n {response}");
|
|
||||||
|
|
||||||
_cache = new RegistrationCache {
|
|
||||||
Location = account.Location,
|
|
||||||
AccountKey = accountKey.ExportCspBlob(true),
|
|
||||||
Id = account.Id,
|
|
||||||
Key = account.Key
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -139,16 +166,25 @@ 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.
|
||||||
/// <para>
|
/// <para>
|
||||||
@ -164,82 +200,104 @@ 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) {
|
||||||
_challenges.Clear();
|
try {
|
||||||
|
|
||||||
var letsEncryptOrder = new Order {
|
_logger.LogInformation($"Executing {nameof(NewOrder)}...");
|
||||||
Expires = DateTime.UtcNow.AddDays(2),
|
|
||||||
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
|
|
||||||
Type = "dns",
|
|
||||||
Value = hostname
|
|
||||||
}).ToArray()
|
|
||||||
};
|
|
||||||
|
|
||||||
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
|
_challenges.Clear();
|
||||||
|
|
||||||
if (order.Status == "ready")
|
var letsEncryptOrder = new Order {
|
||||||
return new Dictionary<string, string>();
|
Expires = DateTime.UtcNow.AddDays(2),
|
||||||
|
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
|
||||||
|
Type = "dns",
|
||||||
|
Value = hostname
|
||||||
|
}).ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
if (order.Status != "pending")
|
var (order, postNewOrderResult) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
|
||||||
throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}");
|
if (!postNewOrderResult.IsSuccess) {
|
||||||
|
return (null, postNewOrderResult);
|
||||||
_currentOrder = order;
|
|
||||||
|
|
||||||
var results = new Dictionary<string, string>();
|
|
||||||
foreach (var item in order.Authorizations) {
|
|
||||||
var (challengeResponse, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, true, null);
|
|
||||||
|
|
||||||
if (challengeResponse.Status == "valid")
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (challengeResponse.Status != "pending")
|
|
||||||
throw new InvalidOperationException($"Expected autorization status 'pending', but got: {order.Status} \r\n {responseText}");
|
|
||||||
|
|
||||||
var challenge = challengeResponse.Challenges.First(x => x.Type == challengeType);
|
|
||||||
_challenges.Add(challenge);
|
|
||||||
|
|
||||||
var keyToken = _jwsService.GetKeyAuthorization(challenge.Token);
|
|
||||||
|
|
||||||
switch (challengeType) {
|
|
||||||
|
|
||||||
// A client fulfills this challenge by constructing a key authorization
|
|
||||||
// from the "token" value provided in the challenge and the client's
|
|
||||||
// account key. The client then computes the SHA-256 digest [FIPS180-4]
|
|
||||||
// of the key authorization.
|
|
||||||
//
|
|
||||||
// The record provisioned to the DNS contains the base64url encoding of
|
|
||||||
// this digest.
|
|
||||||
|
|
||||||
case "dns-01": {
|
|
||||||
using (var sha256 = SHA256.Create()) {
|
|
||||||
var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
|
|
||||||
results[challengeResponse.Identifier.Value] = dnsToken;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// A client fulfills this challenge by constructing a key authorization
|
|
||||||
// from the "token" value provided in the challenge and the client's
|
|
||||||
// account key. The client then provisions the key authorization as a
|
|
||||||
// resource on the HTTP server for the domain in question.
|
|
||||||
//
|
|
||||||
// The path at which the resource is provisioned is comprised of the
|
|
||||||
// fixed prefix "/.well-known/acme-challenge/", followed by the "token"
|
|
||||||
// value in the challenge. The value of the resource MUST be the ASCII
|
|
||||||
// representation of the key authorization.
|
|
||||||
|
|
||||||
case "http-01": {
|
|
||||||
results[challengeResponse.Identifier.Value] = keyToken;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
if (order.Result.Status == "ready")
|
||||||
|
return IDomainResult.Success(new Dictionary<string, string>());
|
||||||
|
|
||||||
|
if (order.Result.Status != "pending") {
|
||||||
|
_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.Result;
|
||||||
|
|
||||||
|
var results = new Dictionary<string, string>();
|
||||||
|
foreach (var item in order.Result.Authorizations) {
|
||||||
|
|
||||||
|
var (challengeResponse, postAuthorisationChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, true, null);
|
||||||
|
if (!postAuthorisationChallengeResult.IsSuccess) {
|
||||||
|
return (null, postAuthorisationChallengeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (challengeResponse.Result.Status == "valid")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (challengeResponse.Result.Status != "pending") {
|
||||||
|
_logger.LogError($"Expected autorization status 'pending', but got: {order.Result.Status} \r\n {challengeResponse.ResponseText}");
|
||||||
|
return IDomainResult.Failed<Dictionary<string, string>?>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var challenge = challengeResponse.Result.Challenges.First(x => x.Type == challengeType);
|
||||||
|
_challenges.Add(challenge);
|
||||||
|
|
||||||
|
var keyToken = _jwsService.GetKeyAuthorization(challenge.Token);
|
||||||
|
|
||||||
|
switch (challengeType) {
|
||||||
|
|
||||||
|
// A client fulfills this challenge by constructing a key authorization
|
||||||
|
// from the "token" value provided in the challenge and the client's
|
||||||
|
// account key. The client then computes the SHA-256 digest [FIPS180-4]
|
||||||
|
// of the key authorization.
|
||||||
|
//
|
||||||
|
// The record provisioned to the DNS contains the base64url encoding of
|
||||||
|
// this digest.
|
||||||
|
|
||||||
|
case "dns-01": {
|
||||||
|
using (var sha256 = SHA256.Create()) {
|
||||||
|
var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
|
||||||
|
results[challengeResponse.Result.Identifier.Value] = dnsToken;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A client fulfills this challenge by constructing a key authorization
|
||||||
|
// from the "token" value provided in the challenge and the client's
|
||||||
|
// account key. The client then provisions the key authorization as a
|
||||||
|
// resource on the HTTP server for the domain in question.
|
||||||
|
//
|
||||||
|
// The path at which the resource is provisioned is comprised of the
|
||||||
|
// fixed prefix "/.well-known/acme-challenge/", followed by the "token"
|
||||||
|
// value in the challenge. The value of the resource MUST be the ASCII
|
||||||
|
// representation of the key authorization.
|
||||||
|
|
||||||
|
case "http-01": {
|
||||||
|
results[challengeResponse.Result.Identifier.Value] = keyToken;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,36 +305,63 @@ 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 {
|
||||||
|
|
||||||
for (var index = 0; index < _challenges.Count; index++) {
|
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
|
||||||
|
|
||||||
var challenge = _challenges[index];
|
if (_currentOrder?.Identifiers == null) {
|
||||||
|
return IDomainResult.Failed();
|
||||||
while (true) {
|
|
||||||
AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge();
|
|
||||||
|
|
||||||
switch (challenge.Type) {
|
|
||||||
case "dns-01": {
|
|
||||||
authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token);
|
|
||||||
//var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, authorizeChallenge, token);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "http-01": {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}");
|
|
||||||
|
|
||||||
if (result.Status == "valid")
|
|
||||||
break;
|
|
||||||
if (result.Status != "pending")
|
|
||||||
throw new InvalidOperationException($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {responseText}");
|
|
||||||
|
|
||||||
await Task.Delay(1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (var index = 0; index < _challenges.Count; index++) {
|
||||||
|
|
||||||
|
var challenge = _challenges[index];
|
||||||
|
|
||||||
|
var start = DateTime.UtcNow;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
var authorizeChallenge = new AuthorizeChallenge();
|
||||||
|
|
||||||
|
switch (challenge.Type) {
|
||||||
|
case "dns-01": {
|
||||||
|
authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token);
|
||||||
|
//var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, authorizeChallenge, token);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "http-01": {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (authChallenge, postAuthChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}");
|
||||||
|
if (!postAuthChallengeResult.IsSuccess) {
|
||||||
|
return postAuthChallengeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authChallenge.Result.Status == "valid")
|
||||||
|
break;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,19 +370,34 @@ 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) {
|
||||||
|
|
||||||
var letsEncryptOrder = new Order {
|
try {
|
||||||
Expires = DateTime.UtcNow.AddDays(2),
|
|
||||||
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
|
|
||||||
Type = "dns",
|
|
||||||
Value = hostname
|
|
||||||
}).ToArray()
|
|
||||||
};
|
|
||||||
|
|
||||||
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
|
_logger.LogInformation($"Executing {nameof(GetOrder)}");
|
||||||
|
|
||||||
_currentOrder = order;
|
var letsEncryptOrder = new Order {
|
||||||
|
Expires = DateTime.UtcNow.AddDays(2),
|
||||||
|
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
|
||||||
|
Type = "dns",
|
||||||
|
Value = hostname
|
||||||
|
}).ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
|
||||||
|
if (!postOrderResult.IsSuccess)
|
||||||
|
return postOrderResult;
|
||||||
|
|
||||||
|
_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,71 +406,87 @@ 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 csr = new CertificateRequest("CN=" + subject,
|
|
||||||
key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
|
||||||
|
|
||||||
var san = new SubjectAlternativeNameBuilder();
|
|
||||||
foreach (var host in _currentOrder.Identifiers)
|
|
||||||
san.AddDnsName(host.Value);
|
|
||||||
|
|
||||||
csr.CertificateExtensions.Add(san.Build());
|
|
||||||
|
|
||||||
var letsEncryptOrder = new FinalizeRequest {
|
|
||||||
Csr = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest())
|
|
||||||
};
|
|
||||||
|
|
||||||
Uri? certificateUrl = default;
|
|
||||||
|
|
||||||
|
|
||||||
var start = DateTime.UtcNow;
|
|
||||||
|
|
||||||
while (certificateUrl == null) {
|
|
||||||
// https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882
|
|
||||||
await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray());
|
|
||||||
|
|
||||||
if (_currentOrder.Status == "ready") {
|
|
||||||
var (response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder);
|
|
||||||
|
|
||||||
if (response.Status == "processing")
|
|
||||||
(response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Location, true, null);
|
|
||||||
|
|
||||||
if (response.Status == "valid") {
|
|
||||||
certificateUrl = response.Certificate;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((start - DateTime.UtcNow).Seconds > 120)
|
var key = new RSACryptoServiceProvider(4096);
|
||||||
throw new TimeoutException();
|
var csr = new CertificateRequest("CN=" + subject,
|
||||||
|
key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||||
|
|
||||||
await Task.Delay(1000);
|
var san = new SubjectAlternativeNameBuilder();
|
||||||
continue;
|
foreach (var host in _currentOrder.Identifiers)
|
||||||
|
san.AddDnsName(host.Value);
|
||||||
|
|
||||||
// throw new InvalidOperationException(/*$"Invalid order status: "*/);
|
csr.CertificateExtensions.Add(san.Build());
|
||||||
|
|
||||||
|
var letsEncryptOrder = new FinalizeRequest {
|
||||||
|
Csr = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest())
|
||||||
|
};
|
||||||
|
|
||||||
|
Uri? certificateUrl = default;
|
||||||
|
|
||||||
|
|
||||||
|
var start = DateTime.UtcNow;
|
||||||
|
|
||||||
|
while (certificateUrl == null) {
|
||||||
|
// https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882
|
||||||
|
await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray());
|
||||||
|
|
||||||
|
if (_currentOrder.Status == "ready") {
|
||||||
|
var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder);
|
||||||
|
if (!postOrderResult.IsSuccess || order?.Result == null)
|
||||||
|
return (null, postOrderResult);
|
||||||
|
|
||||||
|
|
||||||
|
if (order.Result.Status == "processing") {
|
||||||
|
(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 ((DateTime.UtcNow - start).Seconds > 120)
|
||||||
|
throw new TimeoutException();
|
||||||
|
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
var (pem, postPemResult) = await SendAsync<string>(HttpMethod.Post, certificateUrl, true, null);
|
||||||
|
if (!postPemResult.IsSuccess || pem?.Result == null)
|
||||||
|
return (null, postPemResult);
|
||||||
|
|
||||||
|
|
||||||
|
if (_cache == null) {
|
||||||
|
_logger.LogError($"{nameof(_cache)} is null");
|
||||||
|
return IDomainResult.Failed<(X509Certificate2 Cert, RSA PrivateKey)?>();
|
||||||
|
}
|
||||||
|
|
||||||
|
_cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
|
||||||
|
_cache.CachedCerts[subject] = new CertificateCache {
|
||||||
|
Cert = pem.Result,
|
||||||
|
Private = key.ExportCspBlob(true)
|
||||||
|
};
|
||||||
|
|
||||||
|
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem.Result));
|
||||||
|
|
||||||
|
return IDomainResult.Success((cert, key));
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
var (pem, _) = await SendAsync<string>(HttpMethod.Post, certificateUrl, true, null);
|
_logger.LogError(ex, message);
|
||||||
|
return IDomainResult.CriticalDependencyError< (X509Certificate2 Cert, RSA PrivateKey)?>(message);
|
||||||
if (_cache == null)
|
}
|
||||||
throw new NullReferenceException();
|
|
||||||
|
|
||||||
_cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
|
|
||||||
_cache.CachedCerts[subject] = new CertificateCache {
|
|
||||||
Cert = pem,
|
|
||||||
Private = key.ExportCspBlob(true)
|
|
||||||
};
|
|
||||||
|
|
||||||
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem));
|
|
||||||
|
|
||||||
return (cert, key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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,88 +503,128 @@ 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) {
|
||||||
var request = new HttpRequestMessage(method, uri);
|
try {
|
||||||
|
|
||||||
_nonce = uri.OriginalString != "directory"
|
_logger.LogInformation($"Executing {nameof(SendAsync)}...");
|
||||||
? await NewNonce()
|
|
||||||
: default;
|
|
||||||
|
|
||||||
if (message != null || isPostAsGet) {
|
//if (_jwsService == null) {
|
||||||
var jwsHeader = new JwsHeader {
|
// _logger.LogError($"{nameof(_jwsService)} is null");
|
||||||
Url = uri,
|
// return IDomainResult.Failed<SendResult<TResult>?>();
|
||||||
};
|
//}
|
||||||
|
|
||||||
if (_nonce != null)
|
var request = new HttpRequestMessage(method, uri);
|
||||||
jwsHeader.Nonce = _nonce;
|
|
||||||
|
|
||||||
var encodedMessage = isPostAsGet
|
if (uri.OriginalString != "directory") {
|
||||||
? _jwsService.Encode(jwsHeader)
|
var (nonce, newNonceResult) = await NewNonce();
|
||||||
: _jwsService.Encode(message, jwsHeader);
|
if (!newNonceResult.IsSuccess || nonce == null) {
|
||||||
|
return (null, newNonceResult);
|
||||||
|
}
|
||||||
|
|
||||||
var json = encodedMessage.ToJson();
|
_nonce = nonce;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_nonce = default;
|
||||||
|
}
|
||||||
|
|
||||||
request.Content = new StringContent(json);
|
if (requestModel != null || isPostAsGet) {
|
||||||
|
var jwsHeader = new JwsHeader {
|
||||||
|
Url = uri,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_nonce != null)
|
||||||
|
jwsHeader.Nonce = _nonce;
|
||||||
|
|
||||||
|
var encodedMessage = isPostAsGet
|
||||||
|
? _jwsService.Encode(jwsHeader)
|
||||||
|
: _jwsService.Encode(requestModel, jwsHeader);
|
||||||
|
|
||||||
|
var json = encodedMessage.ToJson();
|
||||||
|
|
||||||
|
request.Content = new StringContent(json);
|
||||||
|
|
||||||
|
var requestType = "application/json";
|
||||||
|
if (method == HttpMethod.Post)
|
||||||
|
requestType = "application/jose+json";
|
||||||
|
|
||||||
|
request.Content.Headers.Remove("Content-Type");
|
||||||
|
request.Content.Headers.Add("Content-Type", requestType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
|
||||||
var requestType = "application/json";
|
|
||||||
if (method == HttpMethod.Post)
|
if (method == HttpMethod.Post)
|
||||||
requestType = "application/jose+json";
|
_nonce = response.Headers.GetValues("Replay-Nonce").First();
|
||||||
|
|
||||||
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (response.Content.Headers.ContentType?.MediaType == "application/problem+json")
|
||||||
|
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>();
|
||||||
|
|
||||||
|
if (responseContent is IHasLocation ihl) {
|
||||||
|
if (response.Headers.Location != null)
|
||||||
|
ihl.Location = response.Headers.Location;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IDomainResult.Success(new SendResult<TResult> {
|
||||||
|
Result = responseContent,
|
||||||
|
ResponseText = responseText
|
||||||
|
});
|
||||||
|
|
||||||
request.Content.Headers.Remove("Content-Type");
|
|
||||||
request.Content.Headers.Add("Content-Type", requestType);
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
_logger.LogError(ex, message);
|
||||||
|
return IDomainResult.CriticalDependencyError<SendResult<TResult>?>(message);
|
||||||
if (method == HttpMethod.Post)
|
|
||||||
_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();
|
|
||||||
|
|
||||||
if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") {
|
|
||||||
return ((TResult)(object)responseText, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseContent = responseText.ToObject<TResult>();
|
|
||||||
|
|
||||||
if (responseContent is IHasLocation ihl) {
|
|
||||||
if (response.Headers.Location != null)
|
|
||||||
ihl.Location = response.Headers.Location;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (responseContent, responseText);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Request New Nonce to be able to start POST requests
|
|
||||||
/// </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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,201 +39,197 @@ 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}");
|
|
||||||
|
|
||||||
//define cache folder
|
_logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}");
|
||||||
string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache");
|
|
||||||
if (!Directory.Exists(cachePath)) {
|
//define cache folder
|
||||||
Directory.CreateDirectory(cachePath);
|
string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache");
|
||||||
|
if (!Directory.Exists(cachePath)) {
|
||||||
|
Directory.CreateDirectory(cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
//check acme directory
|
||||||
|
var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme");
|
||||||
|
if (!Directory.Exists(acmePath)) {
|
||||||
|
Directory.CreateDirectory(acmePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
//loop each customer website
|
||||||
|
foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List<Site>()) {
|
||||||
|
_logger.LogInformation($"Managing site: {site.Name}");
|
||||||
|
|
||||||
|
|
||||||
|
//create folder for ssl
|
||||||
|
string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name);
|
||||||
|
if (!Directory.Exists(sslPath)) {
|
||||||
|
Directory.CreateDirectory(sslPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
//check acme directory
|
var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json");
|
||||||
var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme");
|
|
||||||
if (!Directory.Exists(acmePath)) {
|
|
||||||
Directory.CreateDirectory(acmePath);
|
|
||||||
|
#region LetsEncrypt client configuration and local registration cache initialization
|
||||||
|
_logger.LogInformation("1. Client Initialization...");
|
||||||
|
|
||||||
|
await _letsEncryptService.ConfigureClient(env.Url);
|
||||||
|
|
||||||
|
var registrationCache = (File.Exists(cacheFile)
|
||||||
|
? File.ReadAllText(cacheFile)
|
||||||
|
: null)
|
||||||
|
.ToObject<RegistrationCache>();
|
||||||
|
|
||||||
|
var initResult = await _letsEncryptService.Init(customer.Contacts, registrationCache);
|
||||||
|
if (!initResult.IsSuccess) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
//loop each customer website
|
#region LetsEncrypt terms of service
|
||||||
foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List<Site>()) {
|
_logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}");
|
||||||
_logger.LogInformation($"Managing site: {site.Name}");
|
#endregion
|
||||||
|
|
||||||
try {
|
// get cached certificate and check if it's valid
|
||||||
//create folder for ssl
|
// if valid check if cert and key exists otherwise recreate
|
||||||
string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name);
|
// else continue with new certificate request
|
||||||
if (!Directory.Exists(sslPath)) {
|
var certRes = new CachedCertificateResult();
|
||||||
Directory.CreateDirectory(sslPath);
|
if (registrationCache != null && registrationCache.TryGetCachedCertificate(site.Name, out certRes)) {
|
||||||
}
|
|
||||||
|
|
||||||
var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json");
|
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate);
|
||||||
|
|
||||||
//1. Client initialization
|
if (certRes.PrivateKey != null)
|
||||||
_logger.LogInformation("1. Client Initialization...");
|
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem());
|
||||||
|
|
||||||
#region LetsEncrypt client configuration
|
_logger.LogInformation("Certificate and Key exists and valid. Restored from cache.");
|
||||||
await _letsEncryptService.ConfigureClient(env.Url);
|
}
|
||||||
#endregion
|
else {
|
||||||
|
|
||||||
#region LetsEncrypt local registration cache initialization
|
|
||||||
var registrationCache = (File.Exists(cacheFile)
|
|
||||||
? File.ReadAllText(cacheFile)
|
|
||||||
: null)
|
|
||||||
.ToObject<RegistrationCache>();
|
|
||||||
|
|
||||||
await _letsEncryptService.Init(customer.Contacts, registrationCache);
|
//create new orders
|
||||||
#endregion
|
#region LetsEncrypt new order
|
||||||
|
_logger.LogInformation("2. Client New Order...");
|
||||||
|
|
||||||
#region LetsEncrypt terms of service
|
var (orders, newOrderResult) = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge);
|
||||||
_logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}");
|
if (!newOrderResult.IsSuccess || orders == null) {
|
||||||
#endregion
|
continue;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
// get cached certificate and check if it's valid
|
if (orders.Count > 0) {
|
||||||
// if valid check if cert and key exists otherwise recreate
|
switch (site.Challenge) {
|
||||||
// else continue with new certificate request
|
case "http-01": {
|
||||||
var certRes = new CachedCertificateResult();
|
//ensure to enable static file discovery on server in .well-known/acme-challenge
|
||||||
if (registrationCache != null && registrationCache.TryGetCachedCertificate(site.Name, out certRes)) {
|
//and listen on 80 port
|
||||||
|
|
||||||
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate);
|
foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles())
|
||||||
|
file.Delete();
|
||||||
|
|
||||||
if (certRes.PrivateKey != null)
|
foreach (var result in orders) {
|
||||||
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem());
|
Console.WriteLine($"Key: {result.Key}, Value: {result.Value}");
|
||||||
|
string[] splitToken = result.Value.Split('.');
|
||||||
|
|
||||||
_logger.LogInformation("Certificate and Key exists and valid. Restored from cache.");
|
File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value);
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
//try to make new order
|
|
||||||
try {
|
|
||||||
//create new orders
|
|
||||||
Console.WriteLine("2. Client New Order...");
|
|
||||||
|
|
||||||
#region LetsEncrypt new order
|
|
||||||
var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
if (orders.Count > 0) {
|
|
||||||
switch (site.Challenge) {
|
|
||||||
case "http-01": {
|
|
||||||
//ensure to enable static file discovery on server in .well-known/acme-challenge
|
|
||||||
//and listen on 80 port
|
|
||||||
|
|
||||||
foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles())
|
|
||||||
file.Delete();
|
|
||||||
|
|
||||||
foreach (var result in orders) {
|
|
||||||
Console.WriteLine($"Key: {result.Key}, Value: {result.Value}");
|
|
||||||
string[] splitToken = result.Value.Split('.');
|
|
||||||
|
|
||||||
File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) {
|
|
||||||
if (env?.SSH?.Active ?? false) {
|
|
||||||
UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "dns-01": {
|
|
||||||
//Manage DNS server MX record, depends from provider
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) {
|
||||||
#region LetsEncrypt complete challenges
|
|
||||||
_logger.LogInformation("3. Client Complete Challange...");
|
|
||||||
await _letsEncryptService.CompleteChallenges();
|
|
||||||
_logger.LogInformation("Challanges comleted.");
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
await Task.Delay(1000);
|
|
||||||
|
|
||||||
#region Download new certificate
|
|
||||||
_logger.LogInformation("4. Download certificate...");
|
|
||||||
var (cert, key) = await _letsEncryptService.GetCertificate(site.Name);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Persist cache
|
|
||||||
registrationCache = _letsEncryptService.GetRegistrationCache();
|
|
||||||
File.WriteAllText(cacheFile, registrationCache.ToJson());
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Save cert and key to filesystem
|
|
||||||
certRes = new CachedCertificateResult();
|
|
||||||
if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) {
|
|
||||||
|
|
||||||
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate);
|
|
||||||
|
|
||||||
if(certRes.PrivateKey != null)
|
|
||||||
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem());
|
|
||||||
|
|
||||||
_logger.LogInformation("Certificate saved.");
|
|
||||||
|
|
||||||
foreach (FileInfo file in new DirectoryInfo(sslPath).GetFiles()) {
|
|
||||||
|
|
||||||
if (env?.SSH?.Active ?? false) {
|
if (env?.SSH?.Active ?? false) {
|
||||||
UploadFiles(_logger, env.SSH, $"{env.SSL.Linux.Path}/{site.Name}", file.Name, File.ReadAllBytes(file.FullName), env.SSL.Linux.Owner, env.SSL.Linux.ChangeMode);
|
UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
_logger.LogError("Unable to get new cached certificate.");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
_logger.LogError(ex, "");
|
|
||||||
await _letsEncryptService.GetOrder(site.Hosts);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
case "dns-01": {
|
||||||
|
//Manage DNS server MX record, depends from provider
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#region LetsEncrypt complete challenges
|
||||||
|
_logger.LogInformation("3. Client Complete Challange...");
|
||||||
|
var completeChallengesResult = await _letsEncryptService.CompleteChallenges();
|
||||||
|
if (!completeChallengesResult.IsSuccess) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_logger.LogInformation("Challanges comleted.");
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
await Task.Delay(1000);
|
||||||
|
|
||||||
|
#region Download new certificate
|
||||||
|
_logger.LogInformation("4. Download certificate...");
|
||||||
|
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
|
||||||
|
|
||||||
|
#region Persist cache
|
||||||
|
registrationCache = _letsEncryptService.GetRegistrationCache();
|
||||||
|
File.WriteAllText(cacheFile, registrationCache.ToJson());
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Save cert and key to filesystem
|
||||||
|
certRes = new CachedCertificateResult();
|
||||||
|
if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) {
|
||||||
|
|
||||||
|
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate);
|
||||||
|
|
||||||
|
if (certRes.PrivateKey != null)
|
||||||
|
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem());
|
||||||
|
|
||||||
|
_logger.LogInformation("Certificate saved.");
|
||||||
|
|
||||||
|
foreach (FileInfo file in new DirectoryInfo(sslPath).GetFiles()) {
|
||||||
|
|
||||||
|
if (env?.SSH?.Active ?? false) {
|
||||||
|
UploadFiles(_logger, env.SSH, $"{env.SSL.Linux.Path}/{site.Name}", file.Name, File.ReadAllBytes(file.FullName), env.SSL.Linux.Owner, env.SSL.Linux.ChangeMode);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
else {
|
||||||
_logger.LogError(ex, "Customer unhandled error");
|
_logger.LogError("Unable to get new cached certificate.");
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
|
||||||
_logger.LogError(ex, "Environment unhandled error");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (env.Name == "ProductionV2") {
|
_logger.LogInformation($"Let's Encrypt client. Execution complete.");
|
||||||
_terminalService.Exec("systemctl restart nginx");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
_logger.LogError(ex.Message.ToString());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
_logger.LogError(ex, $"Let's Encrypt client. Unhandled exception.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user