From 150b8e76fc15001c0d5ba1041a8d5c9bc0446e78 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Thu, 30 May 2024 22:00:49 +0200 Subject: [PATCH] (feature): .net version upgrade, containerization --- src/.dockerignore | 30 ++ src/Core/Core.csproj | 2 +- src/LetsEncrypt.sln | 16 +- src/LetsEncrypt/LetsEncrypt.csproj | 12 +- .../Services/LetsEncryptService.cs | 359 ++++++++---------- src/LetsEncryptConsole/App.cs | 38 ++ src/LetsEncryptServer/Configuration.cs | 36 ++ .../Controllers/CertsFlowController.cs | 225 +++++++++++ .../Controllers/WellKnownController.cs | 39 ++ src/LetsEncryptServer/Dockerfile | 24 ++ .../LetsEncryptServer.csproj | 27 ++ src/LetsEncryptServer/LetsEncryptServer.http | 6 + .../Models/Requests/InitRequest.cs | 11 + .../Models/Requests/NewOrderRequest.cs | 7 + src/LetsEncryptServer/Program.cs | 28 ++ .../Properties/launchSettings.json | 40 ++ .../appsettings.Development.json | 8 + src/LetsEncryptServer/appsettings.json | 16 + src/SSHProvider/SSHProvider.csproj | 10 +- .../SSHSerivceTests/SSHProviderTests.csproj | 38 +- src/docker-compose.dcproj | 19 + src/docker-compose.override.yml | 9 + src/docker-compose.yml | 8 + src/launchSettings.json | 11 + 24 files changed, 785 insertions(+), 234 deletions(-) create mode 100644 src/.dockerignore create mode 100644 src/LetsEncryptServer/Configuration.cs create mode 100644 src/LetsEncryptServer/Controllers/CertsFlowController.cs create mode 100644 src/LetsEncryptServer/Controllers/WellKnownController.cs create mode 100644 src/LetsEncryptServer/Dockerfile create mode 100644 src/LetsEncryptServer/LetsEncryptServer.csproj create mode 100644 src/LetsEncryptServer/LetsEncryptServer.http create mode 100644 src/LetsEncryptServer/Models/Requests/InitRequest.cs create mode 100644 src/LetsEncryptServer/Models/Requests/NewOrderRequest.cs create mode 100644 src/LetsEncryptServer/Program.cs create mode 100644 src/LetsEncryptServer/Properties/launchSettings.json create mode 100644 src/LetsEncryptServer/appsettings.Development.json create mode 100644 src/LetsEncryptServer/appsettings.json create mode 100644 src/docker-compose.dcproj create mode 100644 src/docker-compose.override.yml create mode 100644 src/docker-compose.yml create mode 100644 src/launchSettings.json diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/src/.dockerignore @@ -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/** \ No newline at end of file diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index fcac937..94e11cb 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 enable enable MaksIT.$(MSBuildProjectName.Replace(" ", "_")) diff --git a/src/LetsEncrypt.sln b/src/LetsEncrypt.sln index dcdab64..62d0dbf 100644 --- a/src/LetsEncrypt.sln +++ b/src/LetsEncrypt.sln @@ -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 diff --git a/src/LetsEncrypt/LetsEncrypt.csproj b/src/LetsEncrypt/LetsEncrypt.csproj index 54bddde..92e7515 100644 --- a/src/LetsEncrypt/LetsEncrypt.csproj +++ b/src/LetsEncrypt/LetsEncrypt.csproj @@ -1,18 +1,18 @@  - net7.0 + net8.0 enable enable MaksIT.$(MSBuildProjectName.Replace(" ", "_")) - - - - - + + + + + diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs index ac27dc2..cd60150 100644 --- a/src/LetsEncrypt/Services/LetsEncryptService.cs +++ b/src/LetsEncrypt/Services/LetsEncryptService.cs @@ -23,44 +23,18 @@ using DomainResults.Common; namespace MaksIT.LetsEncrypt.Services; public interface ILetsEncryptService { - - Task ConfigureClient(string url); - - Task Init(string[] contacts, RegistrationCache? registrationCache); - - RegistrationCache? GetRegistrationCache(); - - (string?, IDomainResult) GetTermsOfServiceUri(); - - - Task<(Dictionary?, IDomainResult)> NewOrder(string[] hostnames, string challengeType); - Task CompleteChallenges(); - Task 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?, List?), IDomainResult)> NewOrder(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, string location, string[] hostnames, string challengeType); + Task CompleteChallenges(Uri newNonce, byte[] accountKeyBytes, string location, Order currentOrder, List _challenges); + Task<(Order?, IDomainResult)> GetOrder(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, string location, string[] hostnames); + Task<(Dictionary?, 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 _logger; - - private HttpClient _httpClient; - - private IJwsService? _jwsService; - private AcmeDirectory? _directory; - private RegistrationCache? _cache; - - private string? _nonce; - - private List _challenges = new List(); - private Order? _currentOrder; + private readonly HttpClient _httpClient; public LetsEncryptService( ILogger logger, @@ -71,27 +45,28 @@ public class LetsEncryptService : ILetsEncryptService { } + /// /// /// /// /// /// - public async Task ConfigureClient(string url) { + public async Task<(AcmeDirectory?, IDomainResult)> ConfigureClient(string url) { try { _httpClient.BaseAddress ??= new Uri(url); - var (directory, getAcmeDirectoryResult) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null); + var (directory, getAcmeDirectoryResult) = await SendAsync(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(); } } @@ -101,27 +76,14 @@ public class LetsEncryptService : ILetsEncryptService { /// /// /// - public async Task 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(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder); - _jwsService.SetKeyId(account.Result.Location.ToString()); + var (account, postAccuntResult) = await SendAsync(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(); } - _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); - } - } - - /// - /// - /// - /// - public RegistrationCache? GetRegistrationCache() => - _cache; - - /// - /// Just retrive terms of service - /// - /// - /// - public (string?, IDomainResult) GetTermsOfServiceUri() { - try { - - _logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}..."); - - if (_directory == null) { - return IDomainResult.Failed(); - } - - return IDomainResult.Success(_directory.Meta.TermsOfService); - } - catch (Exception ex) { - var message = "Let's Encrypt client unhandled exception"; - - _logger.LogError(ex, message); - return IDomainResult.CriticalDependencyError(message); + return IDomainResult.CriticalDependencyError(message); } } @@ -200,12 +135,19 @@ public class LetsEncryptService : ILetsEncryptService { /// /// /// - public async Task<(Dictionary?, IDomainResult)> NewOrder(string[] hostnames, string challengeType) { + public async Task<((Order?, Dictionary?, List?), 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(); + var challenges = new List(); var letsEncryptOrder = new Order { Expires = DateTime.UtcNow.AddDays(2), @@ -215,41 +157,41 @@ public class LetsEncryptService : ILetsEncryptService { }).ToArray() }; - var (order, postNewOrderResult) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); + var (order, postNewOrderResult) = await SendAsync(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()); + 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?>(); + return IDomainResult.Failed<(Order?, Dictionary?, List?)>(); } - _currentOrder = order.Result; + currentOrder = order.Result; - var results = new Dictionary(); - foreach (var item in order.Result.Authorizations) { + + foreach (var item in currentOrder.Authorizations) { - var (challengeResponse, postAuthorisationChallengeResult) = await SendAsync(HttpMethod.Post, item, true, null); + var (challengeResponse, postAuthorisationChallengeResult) = await SendAsync(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?>(); + _logger.LogError($"Expected autorization status 'pending', but got: {currentOrder.Status} \r\n {challengeResponse.ResponseText}"); + return IDomainResult.Failed<(Order?, Dictionary?, List?)>(); } 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?>(message); + return IDomainResult.CriticalDependencyError<(Order?, Dictionary?, List?)>(message); } } @@ -305,18 +248,22 @@ public class LetsEncryptService : ILetsEncryptService { /// /// /// - public async Task CompleteChallenges() { + public async Task CompleteChallenges(Uri newNonce, byte[] accountKeyBytes, string location, Order currentOrder, List 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(HttpMethod.Post, challenge.Url, authorizeChallenge, token); break; } @@ -335,7 +282,7 @@ public class LetsEncryptService : ILetsEncryptService { } } - var (authChallenge, postAuthChallengeResult) = await SendAsync(HttpMethod.Post, challenge.Url, false, "{}"); + var (authChallenge, postAuthChallengeResult) = await SendAsync(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 { /// /// /// - public async Task 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(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); + var (order, postOrderResult) = await SendAsync(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(message); } } @@ -406,86 +355,96 @@ public class LetsEncryptService : ILetsEncryptService { /// /// Cert and Private key /// - public async Task<((X509Certificate2 Cert, RSA PrivateKey)?, IDomainResult)> GetCertificate(string subject) { + public async Task<(Dictionary?, 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(); - 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(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(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>(); } - 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(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(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(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(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(); - _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?>(message); } } @@ -513,16 +472,13 @@ public class LetsEncryptService : ILetsEncryptService { /// /// /// - 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 { /// /// /// - private async Task<(SendResult?, IDomainResult)> SendAsync(HttpMethod method, Uri uri, bool isPostAsGet, object? requestModel) { + private async Task<(SendResult?, IDomainResult)> SendAsync(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?>(); - //} - 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?>(); + + 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(); diff --git a/src/LetsEncryptConsole/App.cs b/src/LetsEncryptConsole/App.cs index 9d12d13..cd397de 100644 --- a/src/LetsEncryptConsole/App.cs +++ b/src/LetsEncryptConsole/App.cs @@ -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 logger, IOptions 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..."); diff --git a/src/LetsEncryptServer/Configuration.cs b/src/LetsEncryptServer/Configuration.cs new file mode 100644 index 0000000..675188a --- /dev/null +++ b/src/LetsEncryptServer/Configuration.cs @@ -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; } + } +} diff --git a/src/LetsEncryptServer/Controllers/CertsFlowController.cs b/src/LetsEncryptServer/Controllers/CertsFlowController.cs new file mode 100644 index 0000000..4b29a30 --- /dev/null +++ b/src/LetsEncryptServer/Controllers/CertsFlowController.cs @@ -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? 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 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 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 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 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(); + 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 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 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 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(); + } + + +} + diff --git a/src/LetsEncryptServer/Controllers/WellKnownController.cs b/src/LetsEncryptServer/Controllers/WellKnownController.cs new file mode 100644 index 0000000..794105c --- /dev/null +++ b/src/LetsEncryptServer/Controllers/WellKnownController.cs @@ -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 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); + } + +} + diff --git a/src/LetsEncryptServer/Dockerfile b/src/LetsEncryptServer/Dockerfile new file mode 100644 index 0000000..7599bb7 --- /dev/null +++ b/src/LetsEncryptServer/Dockerfile @@ -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"] \ No newline at end of file diff --git a/src/LetsEncryptServer/LetsEncryptServer.csproj b/src/LetsEncryptServer/LetsEncryptServer.csproj new file mode 100644 index 0000000..2c55ec6 --- /dev/null +++ b/src/LetsEncryptServer/LetsEncryptServer.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + Linux + ..\docker-compose.dcproj + + + + + + + + + + + + + + + + + + + diff --git a/src/LetsEncryptServer/LetsEncryptServer.http b/src/LetsEncryptServer/LetsEncryptServer.http new file mode 100644 index 0000000..48e4201 --- /dev/null +++ b/src/LetsEncryptServer/LetsEncryptServer.http @@ -0,0 +1,6 @@ +@LetsEncryptServer_HostAddress = http://localhost:5016 + +GET {{LetsEncryptServer_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/LetsEncryptServer/Models/Requests/InitRequest.cs b/src/LetsEncryptServer/Models/Requests/InitRequest.cs new file mode 100644 index 0000000..459bf25 --- /dev/null +++ b/src/LetsEncryptServer/Models/Requests/InitRequest.cs @@ -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; } + } +} diff --git a/src/LetsEncryptServer/Models/Requests/NewOrderRequest.cs b/src/LetsEncryptServer/Models/Requests/NewOrderRequest.cs new file mode 100644 index 0000000..71d2899 --- /dev/null +++ b/src/LetsEncryptServer/Models/Requests/NewOrderRequest.cs @@ -0,0 +1,7 @@ +namespace MaksIT.LetsEncryptServer.Models.Requests { + public class NewOrderRequest { + public string[] Hostnames { get; set; } + + public string ChallengeType { get; set; } + } +} diff --git a/src/LetsEncryptServer/Program.cs b/src/LetsEncryptServer/Program.cs new file mode 100644 index 0000000..293c729 --- /dev/null +++ b/src/LetsEncryptServer/Program.cs @@ -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(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) { + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/src/LetsEncryptServer/Properties/launchSettings.json b/src/LetsEncryptServer/Properties/launchSettings.json new file mode 100644 index 0000000..0ff7fbc --- /dev/null +++ b/src/LetsEncryptServer/Properties/launchSettings.json @@ -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 + } + } +} \ No newline at end of file diff --git a/src/LetsEncryptServer/appsettings.Development.json b/src/LetsEncryptServer/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/LetsEncryptServer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/LetsEncryptServer/appsettings.json b/src/LetsEncryptServer/appsettings.json new file mode 100644 index 0000000..654f4ba --- /dev/null +++ b/src/LetsEncryptServer/appsettings.json @@ -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" + } +} diff --git a/src/SSHProvider/SSHProvider.csproj b/src/SSHProvider/SSHProvider.csproj index 7a8377a..7ab36da 100644 --- a/src/SSHProvider/SSHProvider.csproj +++ b/src/SSHProvider/SSHProvider.csproj @@ -1,16 +1,16 @@ - net7.0 + net8.0 enable enable - - - - + + + + diff --git a/src/Tests/SSHSerivceTests/SSHProviderTests.csproj b/src/Tests/SSHSerivceTests/SSHProviderTests.csproj index cb4a20a..dd0551b 100644 --- a/src/Tests/SSHSerivceTests/SSHProviderTests.csproj +++ b/src/Tests/SSHSerivceTests/SSHProviderTests.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 enable enable @@ -10,35 +10,35 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - - + + + + + + + + - - - - - - + + + + + + diff --git a/src/docker-compose.dcproj b/src/docker-compose.dcproj new file mode 100644 index 0000000..84e0887 --- /dev/null +++ b/src/docker-compose.dcproj @@ -0,0 +1,19 @@ + + + + 2.1 + Linux + False + 0233e43f-435d-4309-b20c-ecd4bfbd2e63 + LaunchBrowser + {Scheme}://localhost:{ServicePort}/swagger + letsencryptserver + + + + docker-compose.yml + + + + + \ No newline at end of file diff --git a/src/docker-compose.override.yml b/src/docker-compose.override.yml new file mode 100644 index 0000000..11d3452 --- /dev/null +++ b/src/docker-compose.override.yml @@ -0,0 +1,9 @@ +version: '3.4' + +services: + letsencryptserver: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_HTTP_PORTS=8080 + ports: + - "8080:8080" diff --git a/src/docker-compose.yml b/src/docker-compose.yml new file mode 100644 index 0000000..bfae4a9 --- /dev/null +++ b/src/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + letsencryptserver: + image: ${DOCKER_REGISTRY-}letsencryptserver + build: + context: . + dockerfile: LetsEncryptServer/Dockerfile diff --git a/src/launchSettings.json b/src/launchSettings.json new file mode 100644 index 0000000..07d1238 --- /dev/null +++ b/src/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "Docker Compose": { + "commandName": "DockerCompose", + "commandVersion": "1.0", + "serviceActions": { + "letsencryptserver": "StartDebugging" + } + } + } +} \ No newline at end of file