(feature): .net version upgrade, containerization

This commit is contained in:
Maksym Sadovnychyy 2024-05-30 22:00:49 +02:00
parent f311a655cc
commit 150b8e76fc
24 changed files with 785 additions and 234 deletions

30
src/.dockerignore Normal file
View File

@ -0,0 +1,30 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>MaksIT.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>

View File

@ -9,11 +9,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncryptConsole", "LetsE
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "Core\Core.csproj", "{27A58A5F-B52A-44F2-9639-84C6F02EA75D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SSHProvider", "SSHProvider\SSHProvider.csproj", "{B6556305-D728-4368-A22C-93079C236808}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SSHProvider", "SSHProvider\SSHProvider.csproj", "{B6556305-D728-4368-A22C-93079C236808}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3374FDB1-C95E-4103-8E14-5BBF0BDC4E9D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SSHProviderTests", "Tests\SSHSerivceTests\SSHProviderTests.csproj", "{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SSHProviderTests", "Tests\SSHSerivceTests\SSHProviderTests.csproj", "{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LetsEncryptServer", "LetsEncryptServer\LetsEncryptServer.csproj", "{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}"
EndProject
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{0233E43F-435D-4309-B20C-ECD4BFBD2E63}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -41,6 +45,14 @@ Global
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Release|Any CPU.Build.0 = Release|Any CPU
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Release|Any CPU.Build.0 = Release|Any CPU
{0233E43F-435D-4309-B20C-ECD4BFBD2E63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0233E43F-435D-4309-B20C-ECD4BFBD2E63}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0233E43F-435D-4309-B20C-ECD4BFBD2E63}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0233E43F-435D-4309-B20C-ECD4BFBD2E63}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>MaksIT.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DomainResult.Common" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="DomainResult.Common" Version="3.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
</ItemGroup>
<ItemGroup>

View File

@ -23,44 +23,18 @@ using DomainResults.Common;
namespace MaksIT.LetsEncrypt.Services;
public interface ILetsEncryptService {
Task<IDomainResult> ConfigureClient(string url);
Task<IDomainResult> Init(string[] contacts, RegistrationCache? registrationCache);
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);
Task<(AcmeDirectory?, IDomainResult)> ConfigureClient(string url);
Task<(RegistrationCache?, IDomainResult)> Init(Uri newAccount, Uri newNonce, string[] contacts);
Task<((Order?, Dictionary<string, string>?, List<AuthorizationChallenge>?), IDomainResult)> NewOrder(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, string location, string[] hostnames, string challengeType);
Task<IDomainResult> CompleteChallenges(Uri newNonce, byte[] accountKeyBytes, string location, Order currentOrder, List<AuthorizationChallenge> _challenges);
Task<(Order?, IDomainResult)> GetOrder(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, string location, string[] hostnames);
Task<(Dictionary<string, CertificateCache>?, IDomainResult)> GetCertificate(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, Order currentOrder, string location, string [] subjects);
}
public class LetsEncryptService : ILetsEncryptService {
//private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings {
// NullValueHandling = NullValueHandling.Ignore,
// Formatting = Formatting.Indented
//};
private readonly ILogger<LetsEncryptService> _logger;
private HttpClient _httpClient;
private IJwsService? _jwsService;
private AcmeDirectory? _directory;
private RegistrationCache? _cache;
private string? _nonce;
private List<AuthorizationChallenge> _challenges = new List<AuthorizationChallenge>();
private Order? _currentOrder;
private readonly HttpClient _httpClient;
public LetsEncryptService(
ILogger<LetsEncryptService> logger,
@ -71,27 +45,28 @@ public class LetsEncryptService : ILetsEncryptService {
}
/// <summary>
///
/// </summary>
/// <param name="url"></param>
/// <param name="contacts"></param>
/// <returns></returns>
public async Task<IDomainResult> ConfigureClient(string url) {
public async Task<(AcmeDirectory?, IDomainResult)> ConfigureClient(string url) {
try {
_httpClient.BaseAddress ??= new Uri(url);
var (directory, getAcmeDirectoryResult) = 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, null, null, null);
if (!getAcmeDirectoryResult.IsSuccess)
return getAcmeDirectoryResult;
return (null, getAcmeDirectoryResult);
_directory = directory.Result;
var result = directory?.Result;
return IDomainResult.Success();
return IDomainResult.Success(result);
}
catch (Exception ex) {
_logger.LogError(ex, "Let's Encrypt client unhandled exception");
return IDomainResult.CriticalDependencyError();
return IDomainResult.CriticalDependencyError<AcmeDirectory>();
}
}
@ -101,27 +76,14 @@ public class LetsEncryptService : ILetsEncryptService {
/// <param name="contacts"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<IDomainResult> Init(string? [] contacts, RegistrationCache? cache) {
public async Task<(RegistrationCache?, IDomainResult)> Init(Uri newAccount, Uri newNonce, string[] contacts) {
try {
_logger.LogInformation($"Executing {nameof(Init)}...");
if (contacts == null || contacts.Length == 0)
return IDomainResult.Failed();
if (_directory == null)
return IDomainResult.Failed();
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 jwsService = new JwsService(accountKey);
var letsEncryptOrder = new Account {
@ -129,59 +91,32 @@ public class LetsEncryptService : ILetsEncryptService {
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());
var (account, postAccuntResult) = await SendAsync<Account>(HttpMethod.Post, newAccount, false, letsEncryptOrder, accountKey, null, newNonce);
if (!postAccuntResult.IsSuccess || account == null)
return (null, postAccuntResult);
// Probably non necessary here
// 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();
return IDomainResult.Failed<RegistrationCache>();
}
_cache = new RegistrationCache {
var cache = new RegistrationCache {
Location = account.Result.Location,
AccountKey = accountKey.ExportCspBlob(true),
Id = account.Result.Id,
Key = account.Result.Key
};
return IDomainResult.Success();
return IDomainResult.Success(cache);
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public RegistrationCache? GetRegistrationCache() =>
_cache;
/// <summary>
/// Just retrive terms of service
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public (string?, IDomainResult) GetTermsOfServiceUri() {
try {
_logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}...");
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);
return IDomainResult.CriticalDependencyError<RegistrationCache>(message);
}
}
@ -200,12 +135,19 @@ public class LetsEncryptService : ILetsEncryptService {
/// <param name="challengeType"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(string[] hostnames, string challengeType) {
public async Task<((Order?, Dictionary<string, string>?, List<AuthorizationChallenge>?), IDomainResult)> NewOrder(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, string location, string[] hostnames, string challengeType) {
try {
var accountKey = new RSACryptoServiceProvider(4096);
accountKey.ImportCspBlob(accountKeyBytes);
var jwsService = new JwsService(accountKey);
_logger.LogInformation($"Executing {nameof(NewOrder)}...");
_challenges.Clear();
var currentOrder = default(Order);
var results = new Dictionary<string, string>();
var challenges = new List<AuthorizationChallenge>();
var letsEncryptOrder = new Order {
Expires = DateTime.UtcNow.AddDays(2),
@ -215,41 +157,41 @@ public class LetsEncryptService : ILetsEncryptService {
}).ToArray()
};
var (order, postNewOrderResult) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
var (order, postNewOrderResult) = await SendAsync<Order>(HttpMethod.Post, newOrder, false, letsEncryptOrder, accountKey, location, newNonce);
if (!postNewOrderResult.IsSuccess) {
return (null, postNewOrderResult);
return ((null, null, null), postNewOrderResult);
}
if (order.Result.Status == "ready")
return IDomainResult.Success(new Dictionary<string, string>());
return IDomainResult.Success((currentOrder, results, challenges));
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>?>();
return IDomainResult.Failed<(Order?, Dictionary<string, string>?, List<AuthorizationChallenge>?)>();
}
_currentOrder = order.Result;
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);
foreach (var item in currentOrder.Authorizations) {
var (challengeResponse, postAuthorisationChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, true, null, accountKey, location, newNonce);
if (!postAuthorisationChallengeResult.IsSuccess) {
return (null, postAuthorisationChallengeResult);
return ((null, null, 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>?>();
_logger.LogError($"Expected autorization status 'pending', but got: {currentOrder.Status} \r\n {challengeResponse.ResponseText}");
return IDomainResult.Failed<(Order?, Dictionary<string, string>?, List<AuthorizationChallenge>?)>();
}
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);
switch (challengeType) {
@ -263,7 +205,7 @@ public class LetsEncryptService : ILetsEncryptService {
case "dns-01": {
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.Result.Identifier.Value] = dnsToken;
}
break;
@ -290,13 +232,14 @@ public class LetsEncryptService : ILetsEncryptService {
}
}
return IDomainResult.Success(results);
// TODO: reurn challenges
return IDomainResult.Success((currentOrder, results, challenges));
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError<Dictionary<string, string>?>(message);
return IDomainResult.CriticalDependencyError<(Order?, Dictionary<string, string>?, List<AuthorizationChallenge>?)>(message);
}
}
@ -305,18 +248,22 @@ public class LetsEncryptService : ILetsEncryptService {
/// </summary>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<IDomainResult> CompleteChallenges() {
public async Task<IDomainResult> CompleteChallenges(Uri newNonce, byte[] accountKeyBytes, string location, Order currentOrder, List<AuthorizationChallenge> challenges) {
try {
var accountKey = new RSACryptoServiceProvider(4096);
accountKey.ImportCspBlob(accountKeyBytes);
var jwsService = new JwsService(accountKey);
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
if (_currentOrder?.Identifiers == null) {
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;
@ -325,7 +272,7 @@ public class LetsEncryptService : ILetsEncryptService {
switch (challenge.Type) {
case "dns-01": {
authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token);
authorizeChallenge.KeyAuthorization = jwsService.GetKeyAuthorization(challenge.Token);
//var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, authorizeChallenge, token);
break;
}
@ -335,7 +282,7 @@ public class LetsEncryptService : ILetsEncryptService {
}
}
var (authChallenge, postAuthChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}");
var (authChallenge, postAuthChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}", accountKey, location, newNonce);
if (!postAuthChallengeResult.IsSuccess) {
return postAuthChallengeResult;
}
@ -344,7 +291,7 @@ public class LetsEncryptService : ILetsEncryptService {
break;
if (authChallenge.Result.Status != "pending") {
_logger.LogError($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {authChallenge.ResponseText}");
_logger.LogError($"Failed autorization of {currentOrder.Identifiers[index].Value} \r\n {authChallenge.ResponseText}");
return IDomainResult.Failed();
}
@ -370,9 +317,11 @@ public class LetsEncryptService : ILetsEncryptService {
/// </summary>
/// <param name="hostnames"></param>
/// <returns></returns>
public async Task<IDomainResult> GetOrder(string[] hostnames) {
public async Task<(Order?, IDomainResult)> GetOrder(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, string location, string[] hostnames) {
try {
var accountKey = new RSACryptoServiceProvider(4096);
accountKey.ImportCspBlob(accountKeyBytes);
_logger.LogInformation($"Executing {nameof(GetOrder)}");
@ -384,19 +333,19 @@ public class LetsEncryptService : ILetsEncryptService {
}).ToArray()
};
var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, newOrder, false, letsEncryptOrder, accountKey, location, newNonce);
if (!postOrderResult.IsSuccess)
return postOrderResult;
return (null, postOrderResult);
_currentOrder = order.Result;
var currentOrder = order.Result;
return IDomainResult.Success();
return IDomainResult.Success(currentOrder);
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
return IDomainResult.CriticalDependencyError<Order?>(message);
}
}
@ -406,86 +355,96 @@ public class LetsEncryptService : ILetsEncryptService {
/// <param name="subject"></param>
/// <returns>Cert and Private key</returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<((X509Certificate2 Cert, RSA PrivateKey)?, IDomainResult)> GetCertificate(string subject) {
public async Task<(Dictionary<string, CertificateCache>?, IDomainResult)> GetCertificate(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, Order currentOrder, string location, string [] subjects) {
try {
var accountKey = new RSACryptoServiceProvider(4096);
accountKey.ImportCspBlob(accountKeyBytes);
var jwsService = new JwsService(accountKey);
_logger.LogInformation($"Executing {nameof(GetCertificate)}...");
if (_currentOrder == null) {
return IDomainResult.Failed<(X509Certificate2 Cert, RSA PrivateKey)?>();
}
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 cachedCerts = new Dictionary<string, CertificateCache>();
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);
foreach (var subject in subjects) {
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 (currentOrder == null) {
return IDomainResult.Failed<Dictionary<string, CertificateCache>>();
}
if ((DateTime.UtcNow - start).Seconds > 120)
throw new TimeoutException();
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(newOrder, newNonce, accountKeyBytes, location, currentOrder.Identifiers.Select(x => x.Value).ToArray());
if (currentOrder.Status == "ready") {
var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, currentOrder.Finalize, false, letsEncryptOrder, accountKey, location, newNonce);
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, accountKey, location, newNonce);
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, accountKey, location, newNonce);
if (!postPemResult.IsSuccess || pem?.Result == null)
return (null, postPemResult);
cachedCerts.Add(subject, new CertificateCache {
Cert = pem.Result,
Private = key.ExportCspBlob(true)
});
//var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem.Result));
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));
return IDomainResult.Success(cachedCerts);
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError< (X509Certificate2 Cert, RSA PrivateKey)?>(message);
return IDomainResult.CriticalDependencyError<Dictionary<string, CertificateCache>?>(message);
}
}
@ -513,16 +472,13 @@ public class LetsEncryptService : ILetsEncryptService {
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
private async Task<(string?, IDomainResult)> NewNonce() {
private async Task<(string?, IDomainResult)> NewNonce(Uri newNonce) {
try {
_logger.LogInformation($"Executing {nameof(NewNonce)}...");
if (_directory == null)
IDomainResult.Failed();
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce));
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, newNonce));
return IDomainResult.Success(result.Headers.GetValues("Replay-Nonce").First());
}
@ -543,31 +499,32 @@ public class LetsEncryptService : ILetsEncryptService {
/// <param name="requestModel"></param>
/// <param name="token"></param>
/// <returns></returns>
private async Task<(SendResult<TResult>?, IDomainResult)> SendAsync<TResult>(HttpMethod method, Uri uri, bool isPostAsGet, object? requestModel) {
private async Task<(SendResult<TResult>?, IDomainResult)> SendAsync<TResult>(HttpMethod method, Uri uri, bool isPostAsGet, object? requestModel, RSACryptoServiceProvider? accountKey, string? location, Uri? newNonce) {
try {
var _nonce = default(string?);
_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);
if (uri.OriginalString != "directory") {
var (nonce, newNonceResult) = await NewNonce();
var (nonce, newNonceResult) = await NewNonce(newNonce);
if (!newNonceResult.IsSuccess || nonce == null) {
return (null, newNonceResult);
}
_nonce = nonce;
}
else {
_nonce = default;
}
if (requestModel != null || isPostAsGet) {
if (accountKey == null)
return IDomainResult.Failed<SendResult<TResult>?>();
var jwsService = new JwsService(accountKey);
if(location != null)
jwsService.SetKeyId(location);
var jwsHeader = new JwsHeader {
Url = uri,
};
@ -576,8 +533,8 @@ public class LetsEncryptService : ILetsEncryptService {
jwsHeader.Nonce = _nonce;
var encodedMessage = isPostAsGet
? _jwsService.Encode(jwsHeader)
: _jwsService.Encode(requestModel, jwsHeader);
? jwsService.Encode(jwsHeader)
: jwsService.Encode(requestModel, jwsHeader);
var json = encodedMessage.ToJson();

View File

@ -16,6 +16,9 @@ public interface IApp {
Task Run(string[] args);
}
public class App : IApp {
private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
@ -25,6 +28,10 @@ public class App : IApp {
private readonly ILetsEncryptService _letsEncryptService;
private readonly ITerminalService _terminalService;
private static readonly string _registerAccount = "--register-account";
private static readonly string _server = "--server";
private static readonly string _mail = "-m";
public App(
ILogger<App> logger,
IOptions<Configuration> appSettings,
@ -39,6 +46,37 @@ public class App : IApp {
public async Task Run(string[] args) {
var parsedArgs = args.Select(x => x.Split(' ')).ToDictionary(x => x[0].Trim(), x => x[1].Trim());
if (parsedArgs.ContainsKey(_registerAccount)) {
_logger.LogInformation("Registring accoount");
if(!parsedArgs.ContainsKey(_server))
throw new ArgumentNullException("Server is required");
if(!parsedArgs.ContainsKey(_mail))
throw new ArgumentNullException("Mail is required");
var mail = parsedArgs[_mail];
if (parsedArgs[_server] == "staging")
await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/");
else if(parsedArgs[_server] == "production")
await _letsEncryptService.ConfigureClient("https://acme-v02.api.letsencrypt.org/");
else
throw new ArgumentException("Invalid server");
return;
}
try {
_logger.LogInformation("Let's Encrypt client. Started...");

View File

@ -0,0 +1,36 @@
namespace LetsEncryptServer {
public class Site {
public required string Name { get; set; }
public required string[] Hosts { get; set; }
public required string Challenge { get; set; }
}
public class Customer {
private string? _id;
public string Id {
get => _id ?? string.Empty;
set => _id = value;
}
public bool Active { get; set; }
public string[]? Contacts { get; set; }
public string? Name { get; set; }
public string? LastName { get; set; }
public Site[]? Sites { get; set; }
}
public class Server {
public required string Address { get; set; }
public required string PrivateKey { get; set; }
public required string Path { get; set; }
}
public class Configuration {
public required string Production { get; set; }
public required string Staging { get; set; }
public required Server Server { get; set; }
public Customer[]? Customers { get; set; }
}
}

View File

@ -0,0 +1,225 @@
using DomainResults.Mvc;
using MaksIT.LetsEncrypt.Entities;
using MaksIT.LetsEncrypt.Models.Responses;
using MaksIT.LetsEncrypt.Services;
using MaksIT.LetsEncryptServer.Models.Requests;
using Microsoft.AspNetCore.Identity.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace LetsEncryptServer.Controllers;
public class LetsEncryptSession {
public RegistrationCache? RegistrationCache { get; set; }
public Order? CurrentOrder { get; set; }
public List<AuthorizationChallenge>? Challenges { get; set; }
public string[] Hostnames { get; set; }
}
[ApiController]
[Route("[controller]")]
public class CertsFlowController : ControllerBase {
private readonly Configuration _appSettings;
private readonly IMemoryCache _memoryCache;
private readonly ILetsEncryptService _letsEncryptService;
private readonly string _acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme");
private readonly string _certPath = Path.Combine();
MemoryCacheEntryOptions _cacheEntryOptions = new MemoryCacheEntryOptions {
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
SlidingExpiration = TimeSpan.FromMinutes(2)
};
public CertsFlowController(
IOptions<Configuration> appSettings,
IMemoryCache memoryCache,
ILetsEncryptService letsEncryptService
) {
_memoryCache = memoryCache;
_appSettings = appSettings.Value;
_letsEncryptService = letsEncryptService;
if (!Directory.Exists(_acmePath))
Directory.CreateDirectory(_acmePath);
Console.WriteLine(_acmePath);
}
[HttpGet("[action]")]
public async Task<IActionResult> TermsOfService() {
var (config, configResult) = await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/directory");
if (!configResult.IsSuccess || config == null)
return configResult.ToActionResult();
return Ok(config.Meta.TermsOfService);
}
[HttpPost("[action]")]
public async Task<IActionResult> Init([FromBody] InitRequest requestData) {
var (config, configResult) = await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/directory");
if (!configResult.IsSuccess || config == null)
return configResult.ToActionResult();
var (cache, cacheResult) = await _letsEncryptService.Init(config.NewAccount, config.NewNonce, requestData.Contacts);
if(!cacheResult.IsSuccess || cache == null)
return cacheResult.ToActionResult();
var cacheData = new LetsEncryptSession {
RegistrationCache = cache,
};
var accountId = Guid.NewGuid().ToString();
_memoryCache.Set(accountId, cacheData, _cacheEntryOptions);
return Ok(accountId);
}
[HttpPost("[action]/{accountId}")]
public async Task<IActionResult> NewOrder(string accountId, [FromBody] NewOrderRequest requestData) {
var cacheData = (LetsEncryptSession?)_memoryCache.Get(accountId);
if (cacheData?.RegistrationCache?.AccountKey == null)
return BadRequest();
var (config, configResult) = await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/directory");
if (!configResult.IsSuccess || config == null)
return configResult.ToActionResult();
var (orderData, newOrderResult) = await _letsEncryptService.NewOrder(
config.NewOrder,
config.NewNonce,
cacheData.RegistrationCache.AccountKey,
cacheData.RegistrationCache.Location.ToString(),
requestData.Hostnames,
requestData.ChallengeType);
if (!newOrderResult.IsSuccess)
return newOrderResult.ToActionResult();
var(currentOrder, results, challenges) = orderData;
if (results?.Count == 0)
return StatusCode(500);
// TODO: save results to disk
var fullPaths = new List<string>();
foreach (var result in results) {
string[] splitToken = result.Value.Split('.');
System.IO.File.WriteAllText(Path.Combine(_acmePath, splitToken[0]), result.Value);
fullPaths.Add(splitToken[0]);
}
cacheData.CurrentOrder = currentOrder;
cacheData.Challenges = challenges;
cacheData.Hostnames = requestData.Hostnames;
_memoryCache.Set(accountId, cacheData, _cacheEntryOptions);
return Ok(fullPaths);
}
[HttpPut("[action]/{accountId}")]
public async Task<IActionResult> CompleteChallenges(string accountId) {
var cacheData = (LetsEncryptSession?)_memoryCache.Get(accountId);
if (cacheData?.RegistrationCache?.AccountKey == null)
return BadRequest();
var (config, configResult) = await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/directory");
if (!configResult.IsSuccess || config == null)
return configResult.ToActionResult();
var challengeResult = await _letsEncryptService.CompleteChallenges(
config.NewNonce,
cacheData.RegistrationCache.AccountKey,
cacheData.RegistrationCache.Location.ToString(),
cacheData.CurrentOrder,
cacheData.Challenges
);
if (!challengeResult.IsSuccess)
return challengeResult.ToActionResult();
return Ok();
}
[HttpGet("[action]/{accountId}")]
public async Task<IActionResult> GetOrder(string accountId) {
var cacheData = (LetsEncryptSession?)_memoryCache.Get(accountId);
if (cacheData?.RegistrationCache?.AccountKey == null)
return BadRequest();
var (config, configResult) = await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/directory");
if (!configResult.IsSuccess || config == null)
return configResult.ToActionResult();
var (currentOrder, currentOrderResult) = await _letsEncryptService.GetOrder(
config.NewOrder,
config.NewNonce,
cacheData.RegistrationCache.AccountKey,
cacheData.RegistrationCache.Location.ToString(),
cacheData.Hostnames
);
if(!currentOrderResult.IsSuccess)
return currentOrderResult.ToActionResult();
cacheData.CurrentOrder = currentOrder;
_memoryCache.Set(accountId, cacheData, _cacheEntryOptions);
return Ok();
}
[HttpPost("[action]/{accountId}")]
public async Task<IActionResult> GetCertificate(string accountId) {
var cacheData = (LetsEncryptSession?)_memoryCache.Get(accountId);
if (cacheData?.RegistrationCache?.AccountKey == null)
return BadRequest();
var (config, configResult) = await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/directory");
if (!configResult.IsSuccess || config == null)
return configResult.ToActionResult();
var (cachedCerts, certsResult) = await _letsEncryptService.GetCertificate(
config.NewOrder,
config.NewNonce,
cacheData.RegistrationCache.AccountKey,
cacheData.CurrentOrder,
cacheData.RegistrationCache.Location.ToString(),
cacheData.Hostnames
);
if (!certsResult.IsSuccess || cachedCerts == null)
return certsResult.ToActionResult();
// TODO: write certs to filesystem
foreach (var (subject, cachedCert) in cachedCerts) {
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(cachedCert.Cert));
}
if (!certsResult.IsSuccess)
return BadRequest();
return Ok();
}
}

View File

@ -0,0 +1,39 @@
using MaksIT.LetsEncrypt.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace LetsEncryptServer.Controllers;
[ApiController]
[Route(".well-known")]
public class WellKnownController : ControllerBase {
private readonly Configuration _appSettings;
private readonly ILetsEncryptService _letsEncryptService;
private readonly string _acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme");
public WellKnownController(
IOptions<Configuration> appSettings,
ILetsEncryptService letsEncryptService
) {
_appSettings = appSettings.Value;
_letsEncryptService = letsEncryptService;
if (!Directory.Exists(_acmePath))
Directory.CreateDirectory(_acmePath);
}
[HttpGet("acme-challenge/{fileName}")]
public IActionResult AcmeChallenge(string fileName) {
var fileContent = System.IO.File.ReadAllText(Path.Combine(_acmePath, fileName));
if (fileContent == null)
return NotFound();
return Ok(fileContent);
}
}

View File

@ -0,0 +1,24 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["LetsEncryptServer/LetsEncryptServer.csproj", "LetsEncryptServer/"]
RUN dotnet restore "./LetsEncryptServer/LetsEncryptServer.csproj"
COPY . .
WORKDIR "/src/LetsEncryptServer"
RUN dotnet build "./LetsEncryptServer.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./LetsEncryptServer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "LetsEncryptServer.dll"]

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DomainResult" Version="3.2.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LetsEncrypt\LetsEncrypt.csproj" />
<ProjectReference Include="..\SSHProvider\SSHProvider.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models\Responses\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
@LetsEncryptServer_HostAddress = http://localhost:5016
GET {{LetsEncryptServer_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.LetsEncryptServer.Models.Requests {
public class InitRequest {
public string[] Contacts { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace MaksIT.LetsEncryptServer.Models.Requests {
public class NewOrderRequest {
public string[] Hostnames { get; set; }
public string ChallengeType { get; set; }
}
}

View File

@ -0,0 +1,28 @@
using MaksIT.LetsEncrypt.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<ILetsEncryptService, LetsEncryptService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) {
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@ -0,0 +1,40 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5016"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Container (Dockerfile)": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
"environmentVariables": {
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:10248",
"sslPort": 0
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,16 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Configuration": {
"Production": "https://acme-v02.api.letsencrypt.org/directory",
"Staging": "https://acme-staging-v02.api.letsencrypt.org/directory",
"ServerPath": "/etc/haproxy/certs"
}
}

View File

@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DomainResult.Common" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="SSH.NET" Version="2020.0.2" />
<PackageReference Include="DomainResult.Common" Version="3.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="SSH.NET" Version="2024.0.0" />
</ItemGroup>
</Project>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@ -10,35 +10,35 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="Serilog.Enrichers.Span" Version="3.1.0" />
<PackageReference Include="Serilog.Expressions" Version="3.4.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="1.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
</ItemGroup>
<ItemGroup>

19
src/docker-compose.dcproj Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
<PropertyGroup Label="Globals">
<ProjectVersion>2.1</ProjectVersion>
<DockerTargetOS>Linux</DockerTargetOS>
<DockerPublishLocally>False</DockerPublishLocally>
<ProjectGuid>0233e43f-435d-4309-b20c-ecd4bfbd2e63</ProjectGuid>
<DockerLaunchAction>LaunchBrowser</DockerLaunchAction>
<DockerServiceUrl>{Scheme}://localhost:{ServicePort}/swagger</DockerServiceUrl>
<DockerServiceName>letsencryptserver</DockerServiceName>
</PropertyGroup>
<ItemGroup>
<None Include="docker-compose.override.yml">
<DependentUpon>docker-compose.yml</DependentUpon>
</None>
<None Include="docker-compose.yml" />
<None Include=".dockerignore" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,9 @@
version: '3.4'
services:
letsencryptserver:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_HTTP_PORTS=8080
ports:
- "8080:8080"

8
src/docker-compose.yml Normal file
View File

@ -0,0 +1,8 @@
version: '3.4'
services:
letsencryptserver:
image: ${DOCKER_REGISTRY-}letsencryptserver
build:
context: .
dockerfile: LetsEncryptServer/Dockerfile

11
src/launchSettings.json Normal file
View File

@ -0,0 +1,11 @@
{
"profiles": {
"Docker Compose": {
"commandName": "DockerCompose",
"commandVersion": "1.0",
"serviceActions": {
"letsencryptserver": "StartDebugging"
}
}
}
}