diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 5e15896..fcac937 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -8,7 +8,9 @@ - + + + diff --git a/src/Core/Extensions/ObjectExtensions.cs b/src/Core/Extensions/ObjectExtensions.cs index d604210..0c5f2b1 100644 --- a/src/Core/Extensions/ObjectExtensions.cs +++ b/src/Core/Extensions/ObjectExtensions.cs @@ -1,37 +1,36 @@ using System.Text.Json.Serialization; using System.Text.Json; -namespace MaksIT.Core.Extensions { - public static class ObjectExtensions { +namespace MaksIT.Core.Extensions; +public static class ObjectExtensions { - /// - /// - /// - /// - /// - /// - public static string ToJson(this T? obj) => obj.ToJson(null); + /// + /// + /// + /// + /// + /// + public static string ToJson(this T? obj) => obj.ToJson(null); - /// - /// - /// - /// - /// - /// - /// - public static string ToJson(this T? obj, List? converters) { - if (obj == null) - return "{}"; + /// + /// + /// + /// + /// + /// + /// + public static string ToJson(this T? obj, List? converters) { + if (obj == null) + return "{}"; - var options = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = true - }; + var options = new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true + }; - converters?.ForEach(x => options.Converters.Add(x)); + converters?.ForEach(x => options.Converters.Add(x)); - return JsonSerializer.Serialize(obj, options); - } + return JsonSerializer.Serialize(obj, options); } } diff --git a/src/Core/Extensions/StringExtensions.cs b/src/Core/Extensions/StringExtensions.cs index a8d5126..fb178e3 100644 --- a/src/Core/Extensions/StringExtensions.cs +++ b/src/Core/Extensions/StringExtensions.cs @@ -6,35 +6,34 @@ using System.Text.Json.Serialization; using System.Text.Json; using System.Threading.Tasks; -namespace MaksIT.Core.Extensions { - public static class StringExtensions { - /// - /// Converts JSON string to object - /// - /// - /// - /// - public static T? ToObject(this string? s) => ToObjectCore(s, null); +namespace MaksIT.Core.Extensions; +public static class StringExtensions { + /// + /// Converts JSON string to object + /// + /// + /// + /// + public static T? ToObject(this string? s) => ToObjectCore(s, null); - /// - /// - /// - /// - /// - /// - /// - public static T? ToObject(this string? s, List converters) => ToObjectCore(s, converters); + /// + /// + /// + /// + /// + /// + /// + public static T? ToObject(this string? s, List converters) => ToObjectCore(s, converters); - private static T? ToObjectCore(string? s, List? converters) { - var options = new JsonSerializerOptions { - PropertyNameCaseInsensitive = true - }; + private static T? ToObjectCore(string? s, List? converters) { + var options = new JsonSerializerOptions { + PropertyNameCaseInsensitive = true + }; - converters?.ForEach(x => options.Converters.Add(x)); + converters?.ForEach(x => options.Converters.Add(x)); - return s != null - ? JsonSerializer.Deserialize(s, options) - : default; - } + return s != null + ? JsonSerializer.Deserialize(s, options) + : default; } } diff --git a/src/Core/OperatingSystem.cs b/src/Core/OperatingSystem.cs index 162e2c1..fb45bec 100644 --- a/src/Core/OperatingSystem.cs +++ b/src/Core/OperatingSystem.cs @@ -1,14 +1,13 @@ using System.Runtime.InteropServices; -namespace MaksIT.Core { - public static class OperatingSystem { - public static bool IsWindows() => - RuntimeInformation.IsOSPlatform(OSPlatform.Windows); +namespace MaksIT.Core; +public static class OperatingSystem { + public static bool IsWindows() => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - public static bool IsMacOS() => - RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + public static bool IsMacOS() => + RuntimeInformation.IsOSPlatform(OSPlatform.OSX); - public static bool IsLinux() => - RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - } + public static bool IsLinux() => + RuntimeInformation.IsOSPlatform(OSPlatform.Linux); } diff --git a/src/LetsEncrypt/Entities/Jws/Jwk.cs b/src/LetsEncrypt/Entities/Jws/Jwk.cs index 2b4d807..0907727 100644 --- a/src/LetsEncrypt/Entities/Jws/Jwk.cs +++ b/src/LetsEncrypt/Entities/Jws/Jwk.cs @@ -2,104 +2,103 @@ using System.Text.Json.Serialization; -namespace MaksIT.LetsEncrypt.Entities.Jws { - public class Jwk { - /// - /// "kty" (Key Type) Parameter - /// - /// The "kty" (key type) parameter identifies the cryptographic algorithm - /// family used with the key, such as "RSA" or "EC". - /// - /// - [JsonPropertyName("kty")] - public string? KeyType { get; set; } +namespace MaksIT.LetsEncrypt.Entities.Jws; +public class Jwk { + /// + /// "kty" (Key Type) Parameter + /// + /// The "kty" (key type) parameter identifies the cryptographic algorithm + /// family used with the key, such as "RSA" or "EC". + /// + /// + [JsonPropertyName("kty")] + public string? KeyType { get; set; } - /// - /// "kid" (Key ID) Parameter - /// - /// The "kid" (key ID) parameter is used to match a specific key. This - /// is used, for instance, to choose among a set of keys within a JWK Set - /// during key rollover. The structure of the "kid" value is - /// unspecified. - /// - /// - [JsonPropertyName("kid")] - public string? KeyId { get; set; } + /// + /// "kid" (Key ID) Parameter + /// + /// The "kid" (key ID) parameter is used to match a specific key. This + /// is used, for instance, to choose among a set of keys within a JWK Set + /// during key rollover. The structure of the "kid" value is + /// unspecified. + /// + /// + [JsonPropertyName("kid")] + public string? KeyId { get; set; } - /// - /// "use" (Public Key Use) Parameter - /// - /// The "use" (public key use) parameter identifies the intended use of - /// the public key. The "use" parameter is employed to indicate whether - /// a public key is used for encrypting data or verifying the signature - /// on data. - /// - /// - [JsonPropertyName("use")] - public string? Use { get; set; } + /// + /// "use" (Public Key Use) Parameter + /// + /// The "use" (public key use) parameter identifies the intended use of + /// the public key. The "use" parameter is employed to indicate whether + /// a public key is used for encrypting data or verifying the signature + /// on data. + /// + /// + [JsonPropertyName("use")] + public string? Use { get; set; } - /// - /// The the modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. - /// - [JsonPropertyName("n")] - public string? Modulus { get; set; } + /// + /// The the modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. + /// + [JsonPropertyName("n")] + public string? Modulus { get; set; } - /// - /// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. - /// - [JsonPropertyName("e")] - public string? Exponent { get; set; } + /// + /// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. + /// + [JsonPropertyName("e")] + public string? Exponent { get; set; } - /// - /// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("d")] - public string? D { get; set; } + /// + /// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("d")] + public string? D { get; set; } - /// - /// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("p")] - public string? P { get; set; } + /// + /// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("p")] + public string? P { get; set; } - /// - /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("q")] - public string? Q { get; set; } + /// + /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("q")] + public string? Q { get; set; } - /// - /// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("dp")] - public string? DP { get; set; } + /// + /// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("dp")] + public string? DP { get; set; } - /// - /// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("dq")] - public string? DQ { get; set; } + /// + /// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("dq")] + public string? DQ { get; set; } - /// - /// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("qi")] - public string? InverseQ { get; set; } + /// + /// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("qi")] + public string? InverseQ { get; set; } - /// - /// The other primes information, should they exist, null or an empty list if not specified. - /// - [JsonPropertyName("oth")] - public string? OthInf { get; set; } + /// + /// The other primes information, should they exist, null or an empty list if not specified. + /// + [JsonPropertyName("oth")] + public string? OthInf { get; set; } - /// - /// "alg" (Algorithm) Parameter - /// - /// The "alg" (algorithm) parameter identifies the algorithm intended for - /// use with the key. - /// - /// - [JsonPropertyName("alg")] - public string? Algorithm { get; set; } - } -} \ No newline at end of file + /// + /// "alg" (Algorithm) Parameter + /// + /// The "alg" (algorithm) parameter identifies the algorithm intended for + /// use with the key. + /// + /// + [JsonPropertyName("alg")] + public string? Algorithm { get; set; } +} diff --git a/src/LetsEncrypt/Entities/Jws/JwsMessage.cs b/src/LetsEncrypt/Entities/Jws/JwsMessage.cs index afd3d25..4726d5e 100644 --- a/src/LetsEncrypt/Entities/Jws/JwsMessage.cs +++ b/src/LetsEncrypt/Entities/Jws/JwsMessage.cs @@ -1,40 +1,36 @@ using System; using System.Text.Json.Serialization; -namespace MaksIT.LetsEncrypt.Entities.Jws -{ - - public class JwsMessage { - - public string? Protected { get; set; } - - public string? Payload { get; set; } - - public string? Signature { get; set; } - } +namespace MaksIT.LetsEncrypt.Entities.Jws; - public class JwsHeader { - - [JsonPropertyName("alg")] - public string? Algorithm { get; set; } - - [JsonPropertyName("jwk")] - public Jwk? Key { get; set; } - - - [JsonPropertyName("kid")] - public string? KeyId { get; set; } - - public string? Nonce { get; set; } - - public Uri? Url { get; set; } - - - [JsonPropertyName("Host")] - public string? Host { get; set; } - } +public class JwsMessage { + public string? Protected { get; set; } + public string? Payload { get; set; } + public string? Signature { get; set; } +} + + +public class JwsHeader { + + [JsonPropertyName("alg")] + public string? Algorithm { get; set; } + + [JsonPropertyName("jwk")] + public Jwk? Key { get; set; } + + + [JsonPropertyName("kid")] + public string? KeyId { get; set; } + + public string? Nonce { get; set; } + + public Uri? Url { get; set; } + + + [JsonPropertyName("Host")] + public string? Host { get; set; } } diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs b/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs index decc2ed..c937703 100644 --- a/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs +++ b/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs @@ -1,13 +1,12 @@ using System; -namespace MaksIT.LetsEncrypt.Entities { - public class AuthorizationChallenge { - public Uri? Url { get; set; } +namespace MaksIT.LetsEncrypt.Entities; +public class AuthorizationChallenge { + public Uri? Url { get; set; } - public string? Type { get; set; } + public string? Type { get; set; } - public string? Status { get; set; } + public string? Status { get; set; } - public string? Token { get; set; } - } + public string? Token { get; set; } } diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs b/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs index abb7436..5b8fbe2 100644 --- a/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs +++ b/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs @@ -1,11 +1,8 @@ using System.Security.Cryptography; -namespace MaksIT.LetsEncrypt.Entities -{ - public class CachedCertificateResult - { - public RSACryptoServiceProvider? PrivateKey { get; set; } - public string? Certificate { get; set; } - } +namespace MaksIT.LetsEncrypt.Entities; +public class CachedCertificateResult { + public RSACryptoServiceProvider? PrivateKey { get; set; } + public string? Certificate { get; set; } } diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs b/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs index 62b348a..6aee7b4 100644 --- a/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs +++ b/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs @@ -1,18 +1,62 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography; +using System.Text; using MaksIT.LetsEncrypt.Entities.Jws; -namespace MaksIT.LetsEncrypt.Entities { - public class CertificateCache { - public string? Cert { get; set; } - public byte[]? Private { get; set; } +namespace MaksIT.LetsEncrypt.Entities; +public class CertificateCache { + public string? Cert { get; set; } + public byte[]? Private { get; set; } +} + +public class RegistrationCache { + public Dictionary? CachedCerts { get; set; } + public byte[]? AccountKey { get; set; } + public string? Id { get; set; } + public Jwk? Key { get; set; } + public Uri? Location { get; set; } + + /// + /// + /// + /// + /// + /// + public bool TryGetCachedCertificate(string subject, out CachedCertificateResult? value) { + value = null; + + if (CachedCerts == null) + return false; + + if (!CachedCerts.TryGetValue(subject, out var cache)) { + return false; + } + + var cert = new X509Certificate2(Encoding.ASCII.GetBytes(cache.Cert)); + + // if it is about to expire, we need to refresh + if ((cert.NotAfter - DateTime.UtcNow).TotalDays < 30) + return false; + + var rsa = new RSACryptoServiceProvider(4096); + rsa.ImportCspBlob(cache.Private); + + value = new CachedCertificateResult { + Certificate = cache.Cert, + PrivateKey = rsa + }; + return true; } - public class RegistrationCache { - public Dictionary? CachedCerts { get; set; } - public byte[]? AccountKey { get; set; } - public string? Id { get; set; } - public Jwk? Key { get; set; } - public Uri? Location { get; set; } + /// + /// + /// + /// + public void ResetCachedCertificate(IEnumerable hostsToRemove) { + if (CachedCerts != null) + foreach (var host in hostsToRemove) + CachedCerts.Remove(host); } } diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/SendResult.cs b/src/LetsEncrypt/Entities/LetsEncrypt/SendResult.cs new file mode 100644 index 0000000..dd55fd1 --- /dev/null +++ b/src/LetsEncrypt/Entities/LetsEncrypt/SendResult.cs @@ -0,0 +1,10 @@ +namespace MaksIT.LetsEncrypt.Entities { + public class SendResult { + + public TResult? Result { get; set; } + + public string? ResponseText { get; set; } + + + } +} diff --git a/src/LetsEncrypt/Exceptions/LetsEncrytException.cs b/src/LetsEncrypt/Exceptions/LetsEncrytException.cs index 615f312..184dbb9 100644 --- a/src/LetsEncrypt/Exceptions/LetsEncrytException.cs +++ b/src/LetsEncrypt/Exceptions/LetsEncrytException.cs @@ -1,25 +1,19 @@ -using System; -using System.Net.Http; +using MaksIT.Core.Extensions; +using MaksIT.LetsEncrypt.Models.Responses; -namespace MaksIT.LetsEncrypt.Exceptions { - public class LetsEncrytException : Exception { - public LetsEncrytException(Problem problem, HttpResponseMessage response) - : base($"{problem.Type}: {problem.Detail}") { - Problem = problem; - Response = response; - } +namespace MaksIT.LetsEncrypt.Exceptions; +public class LetsEncrytException : Exception { - public Problem Problem { get; } + public Problem? Problem { get; } - public HttpResponseMessage Response { get; } - } + public HttpResponseMessage Response { get; } + public LetsEncrytException( + Problem? problem, + HttpResponseMessage response + ) : base(problem != null ? $"{problem.Type}: {problem.Detail}" : "") { - public class Problem { - public string Type { get; set; } - - public string Detail { get; set; } - - public string RawJson { get; set; } + Problem = problem; + Response = response; } } diff --git a/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs b/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs index 9bcc721..99f3de4 100644 --- a/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs +++ b/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs @@ -2,11 +2,10 @@ using MaksIT.LetsEncrypt.Services; -namespace MaksIT.LetsEncrypt.Extensions { - public static class ServiceCollectionExtensions { - public static void RegisterLetsEncrypt(this IServiceCollection services) { +namespace MaksIT.LetsEncrypt.Extensions; +public static class ServiceCollectionExtensions { + public static void RegisterLetsEncrypt(this IServiceCollection services) { - services.AddHttpClient(); - } + services.AddHttpClient(); } } diff --git a/src/LetsEncrypt/LetsEncrypt.csproj b/src/LetsEncrypt/LetsEncrypt.csproj index ae5e10f..54bddde 100644 --- a/src/LetsEncrypt/LetsEncrypt.csproj +++ b/src/LetsEncrypt/LetsEncrypt.csproj @@ -8,8 +8,9 @@ + - + diff --git a/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs b/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs index 0fcaa1f..b06d292 100644 --- a/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs +++ b/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs @@ -1,5 +1,4 @@ -namespace MaksIT.LetsEncrypt.Models.Interfaces { - public interface IHasLocation { - Uri? Location { get; set; } - } +namespace MaksIT.LetsEncrypt.Models.Interfaces; +public interface IHasLocation { + Uri? Location { get; set; } } diff --git a/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs b/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs index 6ffeb01..de09b97 100644 --- a/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs +++ b/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs @@ -1,7 +1,5 @@ -namespace MaksIT.LetsEncrypt.Models.Requests -{ - public class FinalizeRequest - { - public string? Csr { get; set; } - } +namespace MaksIT.LetsEncrypt.Models.Requests; + +public class FinalizeRequest { + public string? Csr { get; set; } } diff --git a/src/LetsEncrypt/Models/Responses/Account.cs b/src/LetsEncrypt/Models/Responses/Account.cs index 8594c90..b00fae4 100644 --- a/src/LetsEncrypt/Models/Responses/Account.cs +++ b/src/LetsEncrypt/Models/Responses/Account.cs @@ -5,34 +5,33 @@ using MaksIT.LetsEncrypt.Models.Interfaces; * https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-7.3 */ -namespace MaksIT.LetsEncrypt.Models.Responses -{ - public class Account : IHasLocation { +namespace MaksIT.LetsEncrypt.Models.Responses; - public bool TermsOfServiceAgreed { get; set; } +public class Account : IHasLocation { - /* - onlyReturnExisting (optional, boolean): If this field is present - with the value "true", then the server MUST NOT create a new - account if one does not already exist. This allows a client to - look up an account URL based on an account key - */ - public bool OnlyReturnExisting { get; set; } + public bool TermsOfServiceAgreed { get; set; } - public string[]? Contacts { get; set; } + /* + onlyReturnExisting (optional, boolean): If this field is present + with the value "true", then the server MUST NOT create a new + account if one does not already exist. This allows a client to + look up an account URL based on an account key + */ + public bool OnlyReturnExisting { get; set; } - public string? Status { get; set; } + public string[]? Contacts { get; set; } - public string? Id { get; set; } + public string? Status { get; set; } - public DateTime CreatedAt { get; set; } + public string? Id { get; set; } - public Jwk? Key { get; set; } + public DateTime CreatedAt { get; set; } - public string? InitialIp { get; set; } + public Jwk? Key { get; set; } - public Uri? Orders { get; set; } + public string? InitialIp { get; set; } - public Uri? Location { get; set; } - } + public Uri? Orders { get; set; } + + public Uri? Location { get; set; } } diff --git a/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs b/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs index 065e063..fc35900 100644 --- a/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs +++ b/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs @@ -1,29 +1,25 @@ using System; -namespace MaksIT.LetsEncrypt.Models.Responses -{ - public class AcmeDirectory - { - public Uri NewNonce { get; set; } +namespace MaksIT.LetsEncrypt.Models.Responses; +public class AcmeDirectory { + public Uri NewNonce { get; set; } - public Uri NewAccount { get; set; } + public Uri NewAccount { get; set; } - public Uri NewOrder { get; set; } + public Uri NewOrder { get; set; } - // New authorization If the ACME server does not implement pre-authorization - // (Section 7.4.1) it MUST omit the "newAuthz" field of the directory. - // [JsonProperty("newAuthz")] - // public Uri NewAuthz { get; set; } - public Uri RevokeCertificate { get; set; } + // New authorization If the ACME server does not implement pre-authorization + // (Section 7.4.1) it MUST omit the "newAuthz" field of the directory. + // [JsonProperty("newAuthz")] + // public Uri NewAuthz { get; set; } + public Uri RevokeCertificate { get; set; } - public Uri KeyChange { get; set; } + public Uri KeyChange { get; set; } - public AcmeDirectoryMeta Meta { get; set; } - } - - public class AcmeDirectoryMeta - { - public string TermsOfService { get; set; } - } + public AcmeDirectoryMeta Meta { get; set; } } + +public class AcmeDirectoryMeta { + public string TermsOfService { get; set; } +} \ No newline at end of file diff --git a/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs index 211ae81..c6dba8b 100644 --- a/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs +++ b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs @@ -1,20 +1,20 @@  using MaksIT.LetsEncrypt.Entities; -namespace MaksIT.LetsEncrypt.Models.Responses { - public class AuthorizationChallengeResponse { - public OrderIdentifier? Identifier { get; set; } +namespace MaksIT.LetsEncrypt.Models.Responses; - public string? Status { get; set; } +public class AuthorizationChallengeResponse { + public OrderIdentifier? Identifier { get; set; } - public DateTime? Expires { get; set; } + public string? Status { get; set; } - public bool Wildcard { get; set; } + public DateTime? Expires { get; set; } - public AuthorizationChallenge[]? Challenges { get; set; } - } + public bool Wildcard { get; set; } - public class AuthorizeChallenge { - public string? KeyAuthorization { get; set; } - } + public AuthorizationChallenge[]? Challenges { get; set; } +} + +public class AuthorizeChallenge { + public string? KeyAuthorization { get; set; } } diff --git a/src/LetsEncrypt/Models/Responses/Order.cs b/src/LetsEncrypt/Models/Responses/Order.cs index 12d316a..ea2236a 100644 --- a/src/LetsEncrypt/Models/Responses/Order.cs +++ b/src/LetsEncrypt/Models/Responses/Order.cs @@ -1,34 +1,33 @@ using MaksIT.LetsEncrypt.Exceptions; using MaksIT.LetsEncrypt.Models.Interfaces; -namespace MaksIT.LetsEncrypt.Models.Responses { +namespace MaksIT.LetsEncrypt.Models.Responses; - public class OrderIdentifier { - public string? Type { get; set; } +public class OrderIdentifier { + public string? Type { get; set; } - public string? Value { get; set; } + public string? Value { get; set; } - } - - public class Order : IHasLocation { - public Uri? Location { get; set; } - - public string? Status { get; set; } - - public DateTime? Expires { get; set; } - - public OrderIdentifier[]? Identifiers { get; set; } - - public DateTime? NotBefore { get; set; } - - public DateTime? NotAfter { get; set; } - - public Problem? Error { get; set; } - - public Uri[]? Authorizations { get; set; } - - public Uri? Finalize { get; set; } - - public Uri? Certificate { get; set; } - } +} + +public class Order : IHasLocation { + public Uri? Location { get; set; } + + public string? Status { get; set; } + + public DateTime? Expires { get; set; } + + public OrderIdentifier[]? Identifiers { get; set; } + + public DateTime? NotBefore { get; set; } + + public DateTime? NotAfter { get; set; } + + public Problem? Error { get; set; } + + public Uri[]? Authorizations { get; set; } + + public Uri? Finalize { get; set; } + + public Uri? Certificate { get; set; } } diff --git a/src/LetsEncrypt/Models/Responses/Problem.cs b/src/LetsEncrypt/Models/Responses/Problem.cs new file mode 100644 index 0000000..fa7811b --- /dev/null +++ b/src/LetsEncrypt/Models/Responses/Problem.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MaksIT.LetsEncrypt.Models.Responses { + public class Problem { + public string Type { get; set; } + + public string Detail { get; set; } + + public string RawJson { get; set; } + } +} diff --git a/src/LetsEncrypt/Services/JwsService.cs b/src/LetsEncrypt/Services/JwsService.cs index b1d775c..73720f5 100644 --- a/src/LetsEncrypt/Services/JwsService.cs +++ b/src/LetsEncrypt/Services/JwsService.cs @@ -11,102 +11,99 @@ using MaksIT.LetsEncrypt.Entities.Jws; using MaksIT.Core.Extensions; -namespace MaksIT.LetsEncrypt.Services { - public interface IJwsService { - void SetKeyId(string location); +namespace MaksIT.LetsEncrypt.Services; - JwsMessage Encode(JwsHeader protectedHeader); +public interface IJwsService { + void SetKeyId(string location); - JwsMessage Encode(TPayload payload, JwsHeader protectedHeader); + JwsMessage Encode(JwsHeader protectedHeader); - string GetKeyAuthorization(string token); + JwsMessage Encode(TPayload payload, JwsHeader protectedHeader); + + string GetKeyAuthorization(string token); - string Base64UrlEncoded(string s); + string Base64UrlEncoded(string s); - string Base64UrlEncoded(byte[] arg); - } - - - public class JwsService : IJwsService { - - public Jwk _jwk; - private RSA _rsa; - - public JwsService(RSA rsa) { - _rsa = rsa ?? throw new ArgumentNullException(nameof(rsa)); - - var publicParameters = rsa.ExportParameters(false); - - var exp = publicParameters.Exponent ?? throw new ArgumentNullException(nameof(publicParameters.Exponent)); - var mod = publicParameters.Modulus ?? throw new ArgumentNullException(nameof(publicParameters.Modulus)); - - _jwk = new Jwk() { - KeyType = "RSA", - Exponent = Base64UrlEncoded(exp), - Modulus = Base64UrlEncoded(mod), - }; - } - - public void SetKeyId(string location) { - _jwk.KeyId = location; - } - - public JwsMessage Encode(JwsHeader protectedHeader) => - Encode(null, protectedHeader); - - public JwsMessage Encode(T? payload, JwsHeader protectedHeader) { - - protectedHeader.Algorithm = "RS256"; - if (_jwk.KeyId != null) { - protectedHeader.KeyId = _jwk.KeyId; - } - else { - protectedHeader.Key = _jwk; - } - - var message = new JwsMessage { - Payload = "", - Protected = Base64UrlEncoded(protectedHeader.ToJson()) - }; - - if (payload != null) { - if (payload is string stringPayload) - message.Payload = Base64UrlEncoded(stringPayload); - else - message.Payload = Base64UrlEncoded(payload.ToJson()); - } - - - message.Signature = Base64UrlEncoded( - _rsa.SignData(Encoding.ASCII.GetBytes($"{message.Protected}.{message.Payload}"), - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1)); - - return message; - } - - public string GetKeyAuthorization(string token) => - $"{token}.{GetSha256Thumbprint()}"; - - private string GetSha256Thumbprint() { - var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}"; - return Base64UrlEncoded(SHA256.HashData(Encoding.UTF8.GetBytes(json))); - } - - - - public string Base64UrlEncoded(string s) => - Base64UrlEncoded(Encoding.UTF8.GetBytes(s)); - - // https://tools.ietf.org/html/rfc4648#section-5 - public string Base64UrlEncoded(byte[] bytes) => - Convert.ToBase64String(bytes) // Regular base64 encoder - .Split('=').First() // Remove any trailing '='s - .Replace('+', '-') // 62nd char of encoding - .Replace('/', '_'); // 63rd char of encoding - - - - } + string Base64UrlEncoded(byte[] arg); +} + + +public class JwsService : IJwsService { + + public Jwk _jwk; + private RSA _rsa; + + public JwsService(RSA rsa) { + _rsa = rsa ?? throw new ArgumentNullException(nameof(rsa)); + + var publicParameters = rsa.ExportParameters(false); + + var exp = publicParameters.Exponent ?? throw new ArgumentNullException(nameof(publicParameters.Exponent)); + var mod = publicParameters.Modulus ?? throw new ArgumentNullException(nameof(publicParameters.Modulus)); + + _jwk = new Jwk() { + KeyType = "RSA", + Exponent = Base64UrlEncoded(exp), + Modulus = Base64UrlEncoded(mod), + }; + } + + public void SetKeyId(string location) { + _jwk.KeyId = location; + } + + public JwsMessage Encode(JwsHeader protectedHeader) => + Encode(null, protectedHeader); + + public JwsMessage Encode(T? payload, JwsHeader protectedHeader) { + + protectedHeader.Algorithm = "RS256"; + if (_jwk.KeyId != null) { + protectedHeader.KeyId = _jwk.KeyId; + } + else { + protectedHeader.Key = _jwk; + } + + var message = new JwsMessage { + Payload = "", + Protected = Base64UrlEncoded(protectedHeader.ToJson()) + }; + + if (payload != null) { + if (payload is string stringPayload) + message.Payload = Base64UrlEncoded(stringPayload); + else + message.Payload = Base64UrlEncoded(payload.ToJson()); + } + + + message.Signature = Base64UrlEncoded( + _rsa.SignData(Encoding.ASCII.GetBytes($"{message.Protected}.{message.Payload}"), + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1)); + + return message; + } + + public string GetKeyAuthorization(string token) => + $"{token}.{GetSha256Thumbprint()}"; + + private string GetSha256Thumbprint() { + var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}"; + return Base64UrlEncoded(SHA256.HashData(Encoding.UTF8.GetBytes(json))); + } + + + + public string Base64UrlEncoded(string s) => + Base64UrlEncoded(Encoding.UTF8.GetBytes(s)); + + // https://tools.ietf.org/html/rfc4648#section-5 + public string Base64UrlEncoded(byte[] bytes) => + Convert.ToBase64String(bytes) // Regular base64 encoder + .Split('=').First() // Remove any trailing '='s + .Replace('+', '-') // 62nd char of encoding + .Replace('/', '_'); // 63rd char of encoding } diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs index 24c58e1..ac27dc2 100644 --- a/src/LetsEncrypt/Services/LetsEncryptService.cs +++ b/src/LetsEncrypt/Services/LetsEncryptService.cs @@ -18,86 +18,100 @@ using MaksIT.LetsEncrypt.Models.Responses; using MaksIT.LetsEncrypt.Models.Interfaces; using MaksIT.LetsEncrypt.Models.Requests; using MaksIT.LetsEncrypt.Entities.Jws; -using System.Xml; -using System.Diagnostics; +using DomainResults.Common; -namespace MaksIT.LetsEncrypt.Services { +namespace MaksIT.LetsEncrypt.Services; - public interface ILetsEncryptService { +public interface ILetsEncryptService { - Task ConfigureClient(string url); + Task ConfigureClient(string url); - Task Init(string[] contacts, RegistrationCache? registrationCache); + Task Init(string[] contacts, RegistrationCache? registrationCache); - string GetTermsOfServiceUri(); + RegistrationCache? GetRegistrationCache(); + + (string?, IDomainResult) GetTermsOfServiceUri(); - Task> NewOrder(string[] hostnames, string challengeType); - Task CompleteChallenges(); - Task GetOrder(string[] hostnames); - Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject); + Task<(Dictionary?, IDomainResult)> NewOrder(string[] hostnames, string challengeType); + Task CompleteChallenges(); + Task GetOrder(string[] hostnames); + Task<((X509Certificate2 Cert, RSA PrivateKey)?, IDomainResult)> GetCertificate(string subject); +} - RegistrationCache? GetRegistrationCache(); + + + +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; + + public LetsEncryptService( + ILogger logger, + HttpClient httpClient + ) { + _logger = logger; + _httpClient = httpClient; } - - - 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; - - public LetsEncryptService( - ILogger logger, - HttpClient httpClient - ) { - _logger = logger; - _httpClient = httpClient; - } - - - /// - /// - /// - /// - /// - /// - public async Task ConfigureClient(string url) { - + /// + /// + /// + /// + /// + /// + public async Task ConfigureClient(string url) { + try { _httpClient.BaseAddress ??= new Uri(url); - (_directory, _) = 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); + if (!getAcmeDirectoryResult.IsSuccess) + return getAcmeDirectoryResult; - /// - /// Account creation or Initialization from cache - /// - /// - /// - /// - public async Task Init(string? [] contacts, RegistrationCache? cache) { + _directory = directory.Result; + + return IDomainResult.Success(); + } + catch (Exception ex) { + _logger.LogError(ex, "Let's Encrypt client unhandled exception"); + return IDomainResult.CriticalDependencyError(); + } + } + + /// + /// Account creation or Initialization from cache + /// + /// + /// + /// + public async Task Init(string? [] contacts, RegistrationCache? cache) { + + try { + + _logger.LogInformation($"Executing {nameof(Init)}..."); if (contacts == null || contacts.Length == 0) - throw new ArgumentNullException(); + return IDomainResult.Failed(); if (_directory == null) - throw new ArgumentNullException(); + return IDomainResult.Failed(); var accountKey = new RSACryptoServiceProvider(4096); @@ -115,58 +129,82 @@ namespace MaksIT.LetsEncrypt.Services { Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray() }; - var (account, response) = await SendAsync(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder); - _jwsService.SetKeyId(account.Location.ToString()); + var (account, postAccuntResult) = await SendAsync(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder); + _jwsService.SetKeyId(account.Result.Location.ToString()); - if (account.Status != "valid") - throw new InvalidOperationException($"Account status is not valid, was: {account.Status} \r\n {response}"); + if (account.Result.Status != "valid") { + _logger.LogError($"Account status is not valid, was: {account.Result.Status} \r\n {account.ResponseText}"); + return IDomainResult.Failed(); + } _cache = new RegistrationCache { - Location = account.Location, + Location = account.Result.Location, AccountKey = accountKey.ExportCspBlob(true), - Id = account.Id, - Key = account.Key + Id = account.Result.Id, + Key = account.Result.Key }; + + return IDomainResult.Success(); } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; - /// - /// - /// - /// - public RegistrationCache? GetRegistrationCache() => - _cache; - - /// - /// Just retrive terms of service - /// - /// - /// - public string GetTermsOfServiceUri() { - - if (_directory == null) - throw new NullReferenceException(); - - return _directory.Meta.TermsOfService; + _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); + } + } + + /// + /// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange. + /// + /// Available challange types: + /// + /// dns-01 + /// http-01 + /// tls-alpn-01 + /// + /// + /// + /// + /// + /// + /// + public async Task<(Dictionary?, IDomainResult)> NewOrder(string[] hostnames, string challengeType) { + try { + + _logger.LogInformation($"Executing {nameof(NewOrder)}..."); - /// - /// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange. - /// - /// Available challange types: - /// - /// dns-01 - /// http-01 - /// tls-alpn-01 - /// - /// - /// - /// - /// - /// - /// - public async Task> NewOrder(string[] hostnames, string challengeType) { _challenges.Clear(); var letsEncryptOrder = new Order { @@ -177,27 +215,38 @@ namespace MaksIT.LetsEncrypt.Services { }).ToArray() }; - var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); + var (order, postNewOrderResult) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); + if (!postNewOrderResult.IsSuccess) { + return (null, postNewOrderResult); + } - if (order.Status == "ready") - return new Dictionary(); + if (order.Result.Status == "ready") + return IDomainResult.Success(new Dictionary()); - if (order.Status != "pending") - throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}"); - - _currentOrder = order; + 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?>(); + } + + _currentOrder = order.Result; var results = new Dictionary(); - foreach (var item in order.Authorizations) { - var (challengeResponse, responseText) = await SendAsync(HttpMethod.Post, item, true, null); + foreach (var item in order.Result.Authorizations) { - if (challengeResponse.Status == "valid") + var (challengeResponse, postAuthorisationChallengeResult) = await SendAsync(HttpMethod.Post, item, true, null); + if (!postAuthorisationChallengeResult.IsSuccess) { + return (null, postAuthorisationChallengeResult); + } + + if (challengeResponse.Result.Status == "valid") continue; - if (challengeResponse.Status != "pending") - throw new InvalidOperationException($"Expected autorization status 'pending', but got: {order.Status} \r\n {responseText}"); + if (challengeResponse.Result.Status != "pending") { + _logger.LogError($"Expected autorization status 'pending', but got: {order.Result.Status} \r\n {challengeResponse.ResponseText}"); + return IDomainResult.Failed?>(); + } - var challenge = challengeResponse.Challenges.First(x => x.Type == challengeType); + var challenge = challengeResponse.Result.Challenges.First(x => x.Type == challengeType); _challenges.Add(challenge); var keyToken = _jwsService.GetKeyAuthorization(challenge.Token); @@ -215,7 +264,7 @@ namespace MaksIT.LetsEncrypt.Services { case "dns-01": { using (var sha256 = SHA256.Create()) { var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken))); - results[challengeResponse.Identifier.Value] = dnsToken; + results[challengeResponse.Result.Identifier.Value] = dnsToken; } break; } @@ -232,7 +281,7 @@ namespace MaksIT.LetsEncrypt.Services { // representation of the key authorization. case "http-01": { - results[challengeResponse.Identifier.Value] = keyToken; + results[challengeResponse.Result.Identifier.Value] = keyToken; break; } @@ -241,22 +290,38 @@ namespace MaksIT.LetsEncrypt.Services { } } - return results; + return IDomainResult.Success(results); } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; - /// - /// - /// - /// - /// - public async Task CompleteChallenges() { + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError?>(message); + } + } + + /// + /// + /// + /// + /// + public async Task CompleteChallenges() { + try { + + _logger.LogInformation($"Executing {nameof(CompleteChallenges)}..."); + + if (_currentOrder?.Identifiers == null) { + return IDomainResult.Failed(); + } for (var index = 0; index < _challenges.Count; index++) { var challenge = _challenges[index]; + var start = DateTime.UtcNow; + while (true) { - AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge(); + var authorizeChallenge = new AuthorizeChallenge(); switch (challenge.Type) { case "dns-01": { @@ -270,24 +335,46 @@ namespace MaksIT.LetsEncrypt.Services { } } - var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, false, "{}"); + var (authChallenge, postAuthChallengeResult) = await SendAsync(HttpMethod.Post, challenge.Url, false, "{}"); + if (!postAuthChallengeResult.IsSuccess) { + return postAuthChallengeResult; + } - if (result.Status == "valid") + if (authChallenge.Result.Status == "valid") break; - if (result.Status != "pending") - throw new InvalidOperationException($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {responseText}"); + + if (authChallenge.Result.Status != "pending") { + _logger.LogError($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {authChallenge.ResponseText}"); + return IDomainResult.Failed(); + } await Task.Delay(1000); + + if ((DateTime.UtcNow - start).Seconds > 120) + throw new TimeoutException(); } } - } - /// - /// - /// - /// - /// - public async Task GetOrder(string[] hostnames) { + return IDomainResult.Success(); + } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; + + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError(message); + } + } + + /// + /// + /// + /// + /// + public async Task GetOrder(string[] hostnames) { + + try { + + _logger.LogInformation($"Executing {nameof(GetOrder)}"); var letsEncryptOrder = new Order { Expires = DateTime.UtcNow.AddDays(2), @@ -297,24 +384,36 @@ namespace MaksIT.LetsEncrypt.Services { }).ToArray() }; - var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); + var (order, postOrderResult) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); + if (!postOrderResult.IsSuccess) + return postOrderResult; - _currentOrder = order; + _currentOrder = order.Result; + + return IDomainResult.Success(); } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; - /// - /// - /// - /// - /// Cert and Private key - /// - public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) { + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError(message); + } + } - _logger.LogInformation($"Invoked: {nameof(GetCertificate)}"); + /// + /// + /// + /// + /// Cert and Private key + /// + public async Task<((X509Certificate2 Cert, RSA PrivateKey)?, IDomainResult)> GetCertificate(string subject) { + try { + _logger.LogInformation($"Executing {nameof(GetCertificate)}..."); - if (_currentOrder == null) - throw new ArgumentNullException(); + if (_currentOrder == null) { + return IDomainResult.Failed<(X509Certificate2 Cert, RSA PrivateKey)?>(); + } var key = new RSACryptoServiceProvider(4096); var csr = new CertificateRequest("CN=" + subject, @@ -340,76 +439,135 @@ namespace MaksIT.LetsEncrypt.Services { await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray()); if (_currentOrder.Status == "ready") { - var (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder); + var (order, postOrderResult) = await SendAsync(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder); + if (!postOrderResult.IsSuccess || order?.Result == null) + return (null, postOrderResult); - if (response.Status == "processing") - (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Location, true, null); - if (response.Status == "valid") { - certificateUrl = response.Certificate; + 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 ((start - DateTime.UtcNow).Seconds > 120) + if ((DateTime.UtcNow - start).Seconds > 120) throw new TimeoutException(); await Task.Delay(1000); - continue; - - // throw new InvalidOperationException(/*$"Invalid order status: "*/); } - var (pem, _) = await SendAsync(HttpMethod.Post, certificateUrl, true, null); + var (pem, postPemResult) = await SendAsync(HttpMethod.Post, certificateUrl, true, null); + if (!postPemResult.IsSuccess || pem?.Result == null) + return (null, postPemResult); - if (_cache == null) - throw new NullReferenceException(); + + if (_cache == null) { + _logger.LogError($"{nameof(_cache)} is null"); + return IDomainResult.Failed<(X509Certificate2 Cert, RSA PrivateKey)?>(); + } _cache.CachedCerts ??= new Dictionary(); _cache.CachedCerts[subject] = new CertificateCache { - Cert = pem, + Cert = pem.Result, Private = key.ExportCspBlob(true) }; - var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem)); + var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem.Result)); - return (cert, key); + return IDomainResult.Success((cert, key)); } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; - /// - /// - /// - /// - /// - public Task KeyChange() { - throw new NotImplementedException(); + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError< (X509Certificate2 Cert, RSA PrivateKey)?>(message); } + } + + /// + /// + /// + /// + /// + public Task KeyChange() { + throw new NotImplementedException(); + } + + /// + /// + /// + /// + /// + public Task RevokeCertificate() { + throw new NotImplementedException(); + } + + + /// + /// Request New Nonce to be able to start POST requests + /// + /// + /// + private async Task<(string?, IDomainResult)> NewNonce() { + + try { + + _logger.LogInformation($"Executing {nameof(NewNonce)}..."); + + if (_directory == null) + IDomainResult.Failed(); + + var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce)); + return IDomainResult.Success(result.Headers.GetValues("Replay-Nonce").First()); - /// - /// - /// - /// - /// - public Task RevokeCertificate() { - throw new NotImplementedException(); } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; + + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError(message); + } + } + + /// + /// Main method used to send data to LetsEncrypt + /// + /// + /// + /// + /// + /// + /// + private async Task<(SendResult?, IDomainResult)> SendAsync(HttpMethod method, Uri uri, bool isPostAsGet, object? requestModel) { + try { + + _logger.LogInformation($"Executing {nameof(SendAsync)}..."); + + //if (_jwsService == null) { + // _logger.LogError($"{nameof(_jwsService)} is null"); + // return IDomainResult.Failed?>(); + //} - /// - /// Main method used to send data to LetsEncrypt - /// - /// - /// - /// - /// - /// - /// - private async Task<(TResult, string)> SendAsync(HttpMethod method, Uri uri, bool isPostAsGet, object? message) where TResult : class { var request = new HttpRequestMessage(method, uri); - _nonce = uri.OriginalString != "directory" - ? await NewNonce() - : default; + if (uri.OriginalString != "directory") { + var (nonce, newNonceResult) = await NewNonce(); + if (!newNonceResult.IsSuccess || nonce == null) { + return (null, newNonceResult); + } - if (message != null || isPostAsGet) { + _nonce = nonce; + } + else { + _nonce = default; + } + + if (requestModel != null || isPostAsGet) { var jwsHeader = new JwsHeader { Url = uri, }; @@ -419,7 +577,7 @@ namespace MaksIT.LetsEncrypt.Services { var encodedMessage = isPostAsGet ? _jwsService.Encode(jwsHeader) - : _jwsService.Encode(message, jwsHeader); + : _jwsService.Encode(requestModel, jwsHeader); var json = encodedMessage.ToJson(); @@ -438,17 +596,15 @@ namespace MaksIT.LetsEncrypt.Services { if (method == HttpMethod.Post) _nonce = response.Headers.GetValues("Replay-Nonce").First(); - if (response.Content.Headers.ContentType.MediaType == "application/problem+json") { - var problemJson = await response.Content.ReadAsStringAsync(); - var problem = problemJson.ToObject(); - problem.RawJson = problemJson; - throw new LetsEncrytException(problem, response); - } - var responseText = await response.Content.ReadAsStringAsync(); - if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") { - return ((TResult)(object)responseText, null); + if (response.Content.Headers.ContentType?.MediaType == "application/problem+json") + throw new LetsEncrytException(responseText.ToObject(), response); + + if (response.Content.Headers.ContentType?.MediaType == "application/pem-certificate-chain" && typeof(TResult) == typeof(string)) { + return IDomainResult.Success(new SendResult { + Result = (TResult)(object)responseText + }); } var responseContent = responseText.ToObject(); @@ -458,20 +614,17 @@ namespace MaksIT.LetsEncrypt.Services { ihl.Location = response.Headers.Location; } - return (responseContent, responseText); + return IDomainResult.Success(new SendResult { + Result = responseContent, + ResponseText = responseText + }); + } + catch (Exception ex) { + var message = "Let's Encrypt client unhandled exception"; - /// - /// Request New Nonce to be able to start POST requests - /// - /// - /// - private async Task NewNonce() { - if (_directory == null) - throw new NotImplementedException(); - - var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce)); - return result.Headers.GetValues("Replay-Nonce").First(); + _logger.LogError(ex, message); + return IDomainResult.CriticalDependencyError?>(message); } } -} \ No newline at end of file +} diff --git a/src/LetsEncryptConsole/App.cs b/src/LetsEncryptConsole/App.cs index a2a021e..9d12d13 100644 --- a/src/LetsEncryptConsole/App.cs +++ b/src/LetsEncryptConsole/App.cs @@ -1,7 +1,3 @@ -using System.Text; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; - using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -10,314 +6,257 @@ using MaksIT.Core.Extensions; using MaksIT.LetsEncrypt.Services; using MaksIT.LetsEncrypt.Entities; using MaksIT.LetsEncryptConsole.Services; -using SSHProvider; -using Mono.Unix.Native; -using Serilog.Core; -namespace MaksIT.LetsEncryptConsole { +using MaksIT.SSHProvider; - public interface IApp { +namespace MaksIT.LetsEncryptConsole; - Task Run(string[] args); +public interface IApp { + + Task Run(string[] args); +} + +public class App : IApp { + + private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; + + private readonly ILogger _logger; + private readonly Configuration _appSettings; + private readonly ILetsEncryptService _letsEncryptService; + private readonly ITerminalService _terminalService; + + public App( + ILogger logger, + IOptions appSettings, + ILetsEncryptService letsEncryptService, + ITerminalService terminalService + ) { + _logger = logger; + _appSettings = appSettings.Value; + _letsEncryptService = letsEncryptService; + _terminalService = terminalService; } - public class App : IApp { + public async Task Run(string[] args) { - private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; - - private readonly ILogger _logger; - private readonly Configuration _appSettings; - private readonly ILetsEncryptService _letsEncryptService; - private readonly IKeyService _keyService; - private readonly ITerminalService _terminalService; - - public App( - ILogger logger, - IOptions appSettings, - ILetsEncryptService letsEncryptService, - IKeyService keyService, - ITerminalService terminalService - ) { - _logger = logger; - _appSettings = appSettings.Value; - _letsEncryptService = letsEncryptService; - _keyService = keyService; - _terminalService = terminalService; - } - - public async Task Run(string[] args) { - - _logger.LogInformation("Letsencrypt client estarted..."); + try { + _logger.LogInformation("Let's Encrypt client. Started..."); foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List()) { - try { - _logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}"); - //loop all customers - foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List()) { - try { - _logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}"); + _logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}"); - //define cache folder - string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache"); - if (!Directory.Exists(cachePath)) { - Directory.CreateDirectory(cachePath); + //loop all customers + foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List()) { + + _logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}"); + + //define cache folder + string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache"); + if (!Directory.Exists(cachePath)) { + Directory.CreateDirectory(cachePath); + } + + //check acme directory + var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme"); + if (!Directory.Exists(acmePath)) { + Directory.CreateDirectory(acmePath); + } + + //loop each customer website + foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List()) { + _logger.LogInformation($"Managing site: {site.Name}"); + + + //create folder for ssl + string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name); + if (!Directory.Exists(sslPath)) { + Directory.CreateDirectory(sslPath); + } + + var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json"); + + + + #region LetsEncrypt client configuration and local registration cache initialization + _logger.LogInformation("1. Client Initialization..."); + + await _letsEncryptService.ConfigureClient(env.Url); + + var registrationCache = (File.Exists(cacheFile) + ? File.ReadAllText(cacheFile) + : null) + .ToObject(); + + var initResult = await _letsEncryptService.Init(customer.Contacts, registrationCache); + if (!initResult.IsSuccess) { + continue; + } + #endregion + + #region LetsEncrypt terms of service + _logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}"); + #endregion + + // get cached certificate and check if it's valid + // if valid check if cert and key exists otherwise recreate + // else continue with new certificate request + var certRes = new CachedCertificateResult(); + if (registrationCache != null && registrationCache.TryGetCachedCertificate(site.Name, out certRes)) { + + File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate); + + if (certRes.PrivateKey != null) + File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem()); + + _logger.LogInformation("Certificate and Key exists and valid. Restored from cache."); + } + else { + + + //create new orders + #region LetsEncrypt new order + _logger.LogInformation("2. Client New Order..."); + + var (orders, newOrderResult) = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge); + if (!newOrderResult.IsSuccess || orders == null) { + continue; + } + #endregion + + if (orders.Count > 0) { + switch (site.Challenge) { + case "http-01": { + //ensure to enable static file discovery on server in .well-known/acme-challenge + //and listen on 80 port + + foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) + file.Delete(); + + foreach (var result in orders) { + Console.WriteLine($"Key: {result.Key}, Value: {result.Value}"); + string[] splitToken = result.Value.Split('.'); + + File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value); + } + + foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) { + if (env?.SSH?.Active ?? false) { + UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode); + } + else { + throw new NotImplementedException(); + } + } + + break; + } + + case "dns-01": { + //Manage DNS server MX record, depends from provider + throw new NotImplementedException(); + } + + default: { + throw new NotImplementedException(); + } + } + + + #region LetsEncrypt complete challenges + _logger.LogInformation("3. Client Complete Challange..."); + var completeChallengesResult = await _letsEncryptService.CompleteChallenges(); + if (!completeChallengesResult.IsSuccess) { + continue; + } + _logger.LogInformation("Challanges comleted."); + #endregion + + await Task.Delay(1000); + + #region Download new certificate + _logger.LogInformation("4. Download certificate..."); + var (certData, getCertResult) = await _letsEncryptService.GetCertificate(site.Name); + if (!getCertResult.IsSuccess || certData == null) { + continue; + } + + // not used in this scenario + // var (cert, key) = certData.Value; + #endregion + + #region Persist cache + registrationCache = _letsEncryptService.GetRegistrationCache(); + File.WriteAllText(cacheFile, registrationCache.ToJson()); + #endregion } - //check acme directory - var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme"); - if (!Directory.Exists(acmePath)) { - Directory.CreateDirectory(acmePath); - } + #region Save cert and key to filesystem + certRes = new CachedCertificateResult(); + if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) { - //loop each customer website - foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List()) { - _logger.LogInformation($"Managing site: {site.Name}"); + File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate); - try { - //create folder for ssl - string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name); - if (!Directory.Exists(sslPath)) { - Directory.CreateDirectory(sslPath); - } + if (certRes.PrivateKey != null) + File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem()); - var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json"); + _logger.LogInformation("Certificate saved."); - //1. Client initialization - _logger.LogInformation("1. Client Initialization..."); + foreach (FileInfo file in new DirectoryInfo(sslPath).GetFiles()) { - #region LetsEncrypt client configuration - await _letsEncryptService.ConfigureClient(env.Url); - #endregion - - #region LetsEncrypt local registration cache initialization - var registrationCache = (File.Exists(cacheFile) - ? File.ReadAllText(cacheFile) - : null) - .ToObject(); - - await _letsEncryptService.Init(customer.Contacts, registrationCache); - #endregion - - #region LetsEncrypt terms of service - _logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}"); - #endregion - - // get cached certificate and check if it's valid - // if valid check if cert and key exists otherwise recreate - // else continue with new certificate request - var certRes = new CachedCertificateResult(); - if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) { - string cert = Path.Combine(sslPath, $"{site.Name}.crt"); - //if(!File.Exists(cert)) - File.WriteAllText(cert, certRes.Certificate); - - string key = Path.Combine(sslPath, $"{site.Name}.key"); - //if(!File.Exists(key)) { - using (StreamWriter writer = File.CreateText(key)) - _keyService.ExportPrivateKey(certRes.PrivateKey, writer); - //} - - _logger.LogInformation("Certificate and Key exists and valid. Restored from cache."); + if (env?.SSH?.Active ?? false) { + UploadFiles(_logger, env.SSH, $"{env.SSL.Linux.Path}/{site.Name}", file.Name, File.ReadAllBytes(file.FullName), env.SSL.Linux.Owner, env.SSL.Linux.ChangeMode); } else { - - //try to make new order - try { - //create new orders - Console.WriteLine("2. Client New Order..."); - - #region LetsEncrypt new order - var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge); - #endregion - - if (orders.Count > 0) { - switch (site.Challenge) { - case "http-01": { - //ensure to enable static file discovery on server in .well-known/acme-challenge - //and listen on 80 port - - foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) - file.Delete(); - - foreach (var result in orders) { - Console.WriteLine($"Key: {result.Key}, Value: {result.Value}"); - string[] splitToken = result.Value.Split('.'); - - File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value); - } - - foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) { - if (env?.SSH?.Active ?? false) { - UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode); - } - else { - throw new NotImplementedException(); - } - } - - - - - - break; - } - - case "dns-01": { - //Manage DNS server MX record, depends from provider - throw new NotImplementedException(); - } - - default: { - throw new NotImplementedException(); - } - } - - - #region LetsEncrypt complete challenges - _logger.LogInformation("3. Client Complete Challange..."); - await _letsEncryptService.CompleteChallenges(); - _logger.LogInformation("Challanges comleted."); - #endregion - - await Task.Delay(1000); - - // Download new certificate - _logger.LogInformation("4. Download certificate..."); - var (cert, key) = await _letsEncryptService.GetCertificate(site.Name); - - #region Persist cache - registrationCache = _letsEncryptService.GetRegistrationCache(); - File.WriteAllText(cacheFile, registrationCache.ToJson()); - #endregion - } - - #region Save cert and key to filesystem - certRes = new CachedCertificateResult(); - if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) { - - File.WriteAllText(Path.Combine(sslPath, site.Name + ".crt"), certRes.Certificate); - - using (var writer = File.CreateText(Path.Combine(sslPath, site.Name + ".key"))) { - _keyService.ExportPrivateKey(certRes.PrivateKey, writer); - } - - _logger.LogInformation("Certificate saved."); - - foreach (FileInfo file in new DirectoryInfo(sslPath).GetFiles()) { - - if (env?.SSH?.Active ?? false) { - UploadFiles(_logger, env.SSH, $"{env.SSL.Linux.Path}/{site.Name}", file.Name, File.ReadAllBytes(file.FullName), env.SSL.Linux.Owner, env.SSL.Linux.ChangeMode); - } - else { - throw new NotImplementedException(); - } - } - - } - else { - _logger.LogError("Unable to get new cached certificate."); - } - #endregion - } - catch (Exception ex) { - _logger.LogError(ex, ""); - await _letsEncryptService.GetOrder(site.Hosts); - } - + throw new NotImplementedException(); } - - - - - } - catch (Exception ex) { - _logger.LogError(ex, "Customer unhandled error"); } + } - } - catch (Exception ex) { - _logger.LogError(ex, "Environment unhandled error"); + else { + _logger.LogError("Unable to get new cached certificate."); + } + #endregion + } } - - if (env.Name == "ProductionV2") { - _terminalService.Exec("systemctl restart nginx"); - } - } - catch (Exception ex) { - _logger.LogError(ex.Message.ToString()); - break; } } + + _logger.LogInformation($"Let's Encrypt client. Execution complete."); } - - /// - /// - /// - /// - /// - /// - private bool TryGetCachedCertificate(RegistrationCache? registrationCache, string subject, out CachedCertificateResult? value) { - value = null; - - if (registrationCache?.CachedCerts == null) - return false; - - if (!registrationCache.CachedCerts.TryGetValue(subject, out var cache)) { - return false; - } - - var cert = new X509Certificate2(Encoding.ASCII.GetBytes(cache.Cert)); - - // if it is about to expire, we need to refresh - if ((cert.NotAfter - DateTime.UtcNow).TotalDays < 30) - return false; - - var rsa = new RSACryptoServiceProvider(4096); - rsa.ImportCspBlob(cache.Private); - - value = new CachedCertificateResult { - Certificate = cache.Cert, - PrivateKey = rsa - }; - return true; + catch (Exception ex) { + _logger.LogError(ex, $"Let's Encrypt client. Unhandled exception."); } - /// - /// - /// - /// - public RegistrationCache? ResetCachedCertificate(RegistrationCache? registrationCache, IEnumerable hostsToRemove) { - if (registrationCache?.CachedCerts != null) - foreach (var host in hostsToRemove) - registrationCache.CachedCerts.Remove(host); + } - return registrationCache; - } - private void UploadFiles( - ILogger logger, - SSHClientSettings sshSettings, - string workDir, - string fileName, - byte [] bytes, - string owner, - string changeMode - ) { - using var sshService = new SSHService(logger, sshSettings.Host, sshSettings.Port, sshSettings.Username, sshSettings.Password); - sshService.Connect(); - sshService.RunSudoCommand(sshSettings.Password, $"mkdir {workDir}"); - sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R"); - sshService.RunSudoCommand(sshSettings.Password, $"chmod 777 {workDir} -R"); + private void UploadFiles( + ILogger logger, + SSHClientSettings sshSettings, + string workDir, + string fileName, + byte[] bytes, + string owner, + string changeMode + ) { + using var sshService = new SSHService(logger, sshSettings.Host, sshSettings.Port, sshSettings.Username, sshSettings.Password); + sshService.Connect(); - sshService.Upload($"{workDir}", fileName, bytes); + sshService.RunSudoCommand(sshSettings.Password, $"mkdir {workDir}"); - sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R"); - sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R"); - } + sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R"); + sshService.RunSudoCommand(sshSettings.Password, $"chmod 777 {workDir} -R"); + + sshService.Upload($"{workDir}", fileName, bytes); + + sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R"); + sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R"); + + //sshService.RunSudoCommand(sshSettings.Password, $"systemctl restart nginx"); } } diff --git a/src/LetsEncryptConsole/Configuration.cs b/src/LetsEncryptConsole/Configuration.cs index afec942..58d0cb1 100644 --- a/src/LetsEncryptConsole/Configuration.cs +++ b/src/LetsEncryptConsole/Configuration.cs @@ -1,40 +1,40 @@ -using System.Runtime.InteropServices; -namespace MaksIT.LetsEncryptConsole { - public class Configuration { - public LetsEncryptEnvironment[]? Environments { get; set; } - public Customer[]? Customers { get; set; } - } +namespace MaksIT.LetsEncryptConsole; - public class OsWindows { - public string? Path { get; set; } - } +public class Configuration { + public LetsEncryptEnvironment[]? Environments { get; set; } + public Customer[]? Customers { get; set; } +} - public class OsLinux { - public string? Path { get; set; } +public class OsWindows { + public string? Path { get; set; } +} - public string? Owner { get; set; } +public class OsLinux { + public string? Path { get; set; } - public string? ChangeMode { get; set; } + public string? Owner { get; set; } - } + public string? ChangeMode { get; set; } - public class OsDependant { - public OsWindows? Windows { get; set; } - public OsLinux? Linux { get; set; } - } +} - public class SSHClientSettings { - public bool Active { get; set; } +public class OsDependant { + public OsWindows? Windows { get; set; } + public OsLinux? Linux { get; set; } +} - public string? Host { get; set; } +public class SSHClientSettings { + public bool Active { get; set; } - public int Port { get; set; } + public string? Host { get; set; } - public string? Username { get; set; } + public int Port { get; set; } - public string? Password { get; set; } - } + public string? Username { get; set; } + + public string? Password { get; set; } +} @@ -69,4 +69,3 @@ namespace MaksIT.LetsEncryptConsole { public string[]? Hosts { get; set; } public string? Challenge { get; set; } } -} diff --git a/src/LetsEncryptConsole/Program.cs b/src/LetsEncryptConsole/Program.cs index 2b205e2..7edba4d 100644 --- a/src/LetsEncryptConsole/Program.cs +++ b/src/LetsEncryptConsole/Program.cs @@ -6,65 +6,64 @@ using Serilog; using MaksIT.LetsEncryptConsole.Services; using MaksIT.LetsEncrypt.Extensions; -namespace MaksIT.LetsEncryptConsole { - class Program { - private static readonly IConfiguration _configuration = InitConfig(); +namespace MaksIT.LetsEncryptConsole; - static void Main(string[] args) { - // create service collection - var services = new ServiceCollection(); - ConfigureServices(services); +class Program { + private static readonly IConfiguration _configuration = InitConfig(); - // create service provider - var serviceProvider = services.BuildServiceProvider(); + static void Main(string[] args) { + // create service collection + var services = new ServiceCollection(); + ConfigureServices(services); - // entry to run app + // create service provider + var serviceProvider = services.BuildServiceProvider(); + + // entry to run app #pragma warning disable CS8602 // Dereference of a possibly null reference. - var app = serviceProvider.GetService(); - app.Run(args).Wait(); + var app = serviceProvider.GetService(); + app.Run(args).Wait(); #pragma warning restore CS8602 // Dereference of a possibly null reference. - } + } - public static void ConfigureServices(IServiceCollection services) { + public static void ConfigureServices(IServiceCollection services) { - var configurationSection = _configuration.GetSection("Configuration"); - services.Configure(configurationSection); - var appSettings = configurationSection.Get(); + var configurationSection = _configuration.GetSection("Configuration"); + services.Configure(configurationSection); + var appSettings = configurationSection.Get(); - #region Configure logging - services.AddLogging(configure => { - configure.AddSerilog(new LoggerConfiguration() - .ReadFrom.Configuration(_configuration) - .CreateLogger()); - }); - #endregion + #region Configure logging + services.AddLogging(configure => { + configure.AddSerilog(new LoggerConfiguration() + .ReadFrom.Configuration(_configuration) + .CreateLogger()); + }); + #endregion - #region Services - services.RegisterLetsEncrypt(); + #region Services + services.RegisterLetsEncrypt(); + services.AddSingleton(); + #endregion - services.AddSingleton(); - services.AddSingleton(); - #endregion + // add app + services.AddSingleton(); + } - // add app - services.AddSingleton(); - } + private static IConfiguration InitConfig() { + var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - private static IConfiguration InitConfig() { - var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddEnvironmentVariables(); - var configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddEnvironmentVariables(); + if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) + && new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), $"appsettings.{aspNetCoreEnvironment}.json")).Exists + ) + configuration.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true); + else + configuration.AddJsonFile($"appsettings.json", true, true); - if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) - && new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), $"appsettings.{aspNetCoreEnvironment}.json")).Exists - ) - configuration.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true); - else - configuration.AddJsonFile($"appsettings.json", true, true); - - return configuration.Build(); - } + return configuration.Build(); } } + diff --git a/src/LetsEncryptConsole/Services/KeyService.cs b/src/LetsEncryptConsole/Services/KeyService.cs deleted file mode 100644 index 26743fc..0000000 --- a/src/LetsEncryptConsole/Services/KeyService.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System.Security.Cryptography; - -namespace MaksIT.LetsEncryptConsole.Services { - - public interface IKeyService { - void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream); - void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream); - } - - public class KeyService : IKeyService { - /// - /// Export a certificate to a PEM format string - /// - /// The certificate to export - /// A PEM encoded string - //public static string ExportToPEM(X509Certificate2 cert) - //{ - // StringBuilder builder = new StringBuilder(); - - // builder.AppendLine("-----BEGIN CERTIFICATE-----"); - // builder.AppendLine(Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)); - // builder.AppendLine("-----END CERTIFICATE-----"); - - // return builder.ToString(); - //} - public void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream) { - var parameters = csp.ExportParameters(false); - using (var stream = new MemoryStream()) { - var writer = new BinaryWriter(stream); - writer.Write((byte)0x30); // SEQUENCE - using (var innerStream = new MemoryStream()) { - var innerWriter = new BinaryWriter(innerStream); - innerWriter.Write((byte)0x30); // SEQUENCE - EncodeLength(innerWriter, 13); - innerWriter.Write((byte)0x06); // OBJECT IDENTIFIER - var rsaEncryptionOid = new byte[] { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01 }; - EncodeLength(innerWriter, rsaEncryptionOid.Length); - innerWriter.Write(rsaEncryptionOid); - innerWriter.Write((byte)0x05); // NULL - EncodeLength(innerWriter, 0); - innerWriter.Write((byte)0x03); // BIT STRING - using (var bitStringStream = new MemoryStream()) { - var bitStringWriter = new BinaryWriter(bitStringStream); - bitStringWriter.Write((byte)0x00); // # of unused bits - bitStringWriter.Write((byte)0x30); // SEQUENCE - using (var paramsStream = new MemoryStream()) { - var paramsWriter = new BinaryWriter(paramsStream); - EncodeIntegerBigEndian(paramsWriter, parameters.Modulus); // Modulus - EncodeIntegerBigEndian(paramsWriter, parameters.Exponent); // Exponent - var paramsLength = (int)paramsStream.Length; - EncodeLength(bitStringWriter, paramsLength); - bitStringWriter.Write(paramsStream.GetBuffer(), 0, paramsLength); - } - var bitStringLength = (int)bitStringStream.Length; - EncodeLength(innerWriter, bitStringLength); - innerWriter.Write(bitStringStream.GetBuffer(), 0, bitStringLength); - } - var length = (int)innerStream.Length; - EncodeLength(writer, length); - writer.Write(innerStream.GetBuffer(), 0, length); - } - - var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray(); - outputStream.WriteLine("-----BEGIN PUBLIC KEY-----"); - for (var i = 0; i < base64.Length; i += 64) { - outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i)); - } - outputStream.WriteLine("-----END PUBLIC KEY-----"); - } - } - - public void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream) { - if (csp.PublicOnly) throw new ArgumentException("CSP does not contain a private key", "csp"); - var parameters = csp.ExportParameters(true); - using (var stream = new MemoryStream()) { - var writer = new BinaryWriter(stream); - writer.Write((byte)0x30); // SEQUENCE - using (var innerStream = new MemoryStream()) { - var innerWriter = new BinaryWriter(innerStream); - EncodeIntegerBigEndian(innerWriter, new byte[] { 0x00 }); // Version - EncodeIntegerBigEndian(innerWriter, parameters.Modulus); - EncodeIntegerBigEndian(innerWriter, parameters.Exponent); - EncodeIntegerBigEndian(innerWriter, parameters.D); - EncodeIntegerBigEndian(innerWriter, parameters.P); - EncodeIntegerBigEndian(innerWriter, parameters.Q); - EncodeIntegerBigEndian(innerWriter, parameters.DP); - EncodeIntegerBigEndian(innerWriter, parameters.DQ); - EncodeIntegerBigEndian(innerWriter, parameters.InverseQ); - var length = (int)innerStream.Length; - EncodeLength(writer, length); - writer.Write(innerStream.GetBuffer(), 0, length); - } - - var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray(); - outputStream.WriteLine("-----BEGIN RSA PRIVATE KEY-----"); - // Output as Base64 with lines chopped at 64 characters - for (var i = 0; i < base64.Length; i += 64) { - outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i)); - } - outputStream.WriteLine("-----END RSA PRIVATE KEY-----"); - } - } - - private void EncodeLength(BinaryWriter stream, int length) { - if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative"); - if (length < 0x80) { - // Short form - stream.Write((byte)length); - } - else { - // Long form - var temp = length; - var bytesRequired = 0; - while (temp > 0) { - temp >>= 8; - bytesRequired++; - } - stream.Write((byte)(bytesRequired | 0x80)); - for (var i = bytesRequired - 1; i >= 0; i--) { - stream.Write((byte)(length >> (8 * i) & 0xff)); - } - } - } - - private void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, bool forceUnsigned = true) { - stream.Write((byte)0x02); // INTEGER - var prefixZeros = 0; - for (var i = 0; i < value.Length; i++) { - if (value[i] != 0) break; - prefixZeros++; - } - if (value.Length - prefixZeros == 0) { - EncodeLength(stream, 1); - stream.Write((byte)0); - } - else { - if (forceUnsigned && value[prefixZeros] > 0x7f) { - // Add a prefix zero to force unsigned if the MSB is 1 - EncodeLength(stream, value.Length - prefixZeros + 1); - stream.Write((byte)0); - } - else { - EncodeLength(stream, value.Length - prefixZeros); - } - for (var i = prefixZeros; i < value.Length; i++) { - stream.Write(value[i]); - } - } - } - } -} \ No newline at end of file diff --git a/src/LetsEncryptConsole/Services/TerminalService.cs b/src/LetsEncryptConsole/Services/TerminalService.cs index f65a119..7607531 100644 --- a/src/LetsEncryptConsole/Services/TerminalService.cs +++ b/src/LetsEncryptConsole/Services/TerminalService.cs @@ -1,30 +1,28 @@ using System.Diagnostics; -namespace MaksIT.LetsEncryptConsole.Services { +namespace MaksIT.LetsEncryptConsole.Services; - public interface ITerminalService { - void Exec(string cmd); - } +public interface ITerminalService { + void Exec(string cmd); +} - public class TerminalService : ITerminalService { +public class TerminalService : ITerminalService { - public void Exec(string cmd) { - var escapedArgs = cmd.Replace("\"", "\\\""); + public void Exec(string cmd) { + var escapedArgs = cmd.Replace("\"", "\\\""); - var pc = new Process { - StartInfo = new ProcessStartInfo { - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - FileName = "/bin/bash", - Arguments = $"-c \"{escapedArgs}\"" - } - }; + var pc = new Process { + StartInfo = new ProcessStartInfo { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + FileName = "/bin/bash", + Arguments = $"-c \"{escapedArgs}\"" + } + }; - pc.Start(); - pc.WaitForExit(); - } + pc.Start(); + pc.WaitForExit(); } - -} \ No newline at end of file +} diff --git a/src/SSHProvider/Configuration.cs b/src/SSHProvider/Configuration.cs index e562e50..7f082ab 100644 --- a/src/SSHProvider/Configuration.cs +++ b/src/SSHProvider/Configuration.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; + +namespace MaksIT.SSHProvider; -namespace SSHProvider { - public class Configuration { - } +public class Configuration { } diff --git a/src/SSHProvider/SSHProvider.csproj b/src/SSHProvider/SSHProvider.csproj index aef8d01..7a8377a 100644 --- a/src/SSHProvider/SSHProvider.csproj +++ b/src/SSHProvider/SSHProvider.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/SSHProvider/SSHService.cs b/src/SSHProvider/SSHService.cs index 89b61e0..419f4b0 100644 --- a/src/SSHProvider/SSHService.cs +++ b/src/SSHProvider/SSHService.cs @@ -1,11 +1,14 @@ -using DomainResults.Common; +using System.Text; +using System.Text.RegularExpressions; + using Microsoft.Extensions.Logging; +using DomainResults.Common; + using Renci.SshNet; using Renci.SshNet.Common; -using System.Text.RegularExpressions; -namespace SSHProvider { +namespace MaksIT.SSHProvider { public interface ISSHService : IDisposable { IDomainResult Upload(string workingdirectory, string fileName, byte[] bytes); @@ -22,8 +25,6 @@ namespace SSHProvider { public readonly SshClient _sshClient; public readonly SftpClient _sftpClient; - - public SSHService( ILogger logger, string host, @@ -31,11 +32,40 @@ namespace SSHProvider { string username, string password ) { + + if(string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + throw new ArgumentNullException($"{nameof(username)} or {nameof(password)} is null, empty or white space"); + _logger = logger; _sshClient = new SshClient(host, port, username, password); _sftpClient = new SftpClient(host, port, username, password); } + + public SSHService( + ILogger logger, + string host, + int port, + string username, + string [] privateKeys + ) { + + if (string.IsNullOrWhiteSpace(username) || privateKeys.Any(x => string.IsNullOrWhiteSpace(x))) + throw new ArgumentNullException($"{nameof(username)} or {nameof(privateKeys)} contains key which is null, empty or white space"); + + _logger = logger; + + var privateKeyFiles = new List(); + foreach (var privateKey in privateKeys) { + using (var ms = new MemoryStream(Encoding.ASCII.GetBytes(privateKey))) { + privateKeyFiles.Add(new PrivateKeyFile(ms)); + } + } + + _sshClient = new SshClient(host, port, username, privateKeyFiles.ToArray()); + _sftpClient = new SftpClient(host, port, username, privateKeyFiles.ToArray()); + } + public IDomainResult Connect() { try { _sshClient.Connect(); @@ -77,8 +107,8 @@ namespace SSHProvider { _logger.LogInformation($"Listing directory:"); - foreach (var fi in listDirectory) { - _logger.LogInformation($" - " + fi.Name); + foreach (var file in listDirectory) { + _logger.LogInformation($" - " + file.Name); } return IDomainResult.Success(); diff --git a/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs b/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs index b67b6ca..147f91e 100644 --- a/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs +++ b/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs @@ -7,57 +7,56 @@ using Xunit; //using PecMgr.VaultProvider; //using PecMgr.Core.Abstractions; -namespace MaksIT.Tests.SSHProviderTests.Abstractions { - //[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] - public abstract class ConfigurationBase { +namespace MaksIT.Tests.SSHProviderTests.Abstractions; +//[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] +public abstract class ConfigurationBase { - protected IConfiguration Configuration; + protected IConfiguration Configuration; - protected ServiceCollection ServiceCollection = new ServiceCollection(); + protected ServiceCollection ServiceCollection = new ServiceCollection(); - protected ServiceProvider ServiceProvider { get => ServiceCollection.BuildServiceProvider(); } + protected ServiceProvider ServiceProvider { get => ServiceCollection.BuildServiceProvider(); } - public ConfigurationBase() { - Configuration = InitConfig(); - ConfigureServices(ServiceCollection); - } + public ConfigurationBase() { + Configuration = InitConfig(); + ConfigureServices(ServiceCollection); + } - protected abstract void ConfigureServices(IServiceCollection services); + protected abstract void ConfigureServices(IServiceCollection services); - private IConfiguration InitConfig() { - var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - var currentDirectory = Directory.GetCurrentDirectory(); + private IConfiguration InitConfig() { + var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + var currentDirectory = Directory.GetCurrentDirectory(); - var configurationBuilder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddEnvironmentVariables(); + var configurationBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddEnvironmentVariables(); - if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) && new FileInfo(Path.Combine(currentDirectory, $"appsettings.{aspNetCoreEnvironment}.json")).Exists) - configurationBuilder.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true); - else if (new FileInfo(Path.Combine(currentDirectory, "appsettings.json")).Exists) - configurationBuilder.AddJsonFile("appsettings.json", true, true); - else - throw new FileNotFoundException($"Unable to find appsetting.json in {currentDirectory}"); + if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) && new FileInfo(Path.Combine(currentDirectory, $"appsettings.{aspNetCoreEnvironment}.json")).Exists) + configurationBuilder.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true); + else if (new FileInfo(Path.Combine(currentDirectory, "appsettings.json")).Exists) + configurationBuilder.AddJsonFile("appsettings.json", true, true); + else + throw new FileNotFoundException($"Unable to find appsetting.json in {currentDirectory}"); - //var builtConfig = configurationBuilder.Build(); - //var vaultOptions = builtConfig.GetSection("Vault"); + //var builtConfig = configurationBuilder.Build(); + //var vaultOptions = builtConfig.GetSection("Vault"); - //configurationBuilder.AddVault(options => { - // options.Address = vaultOptions["Address"]; + //configurationBuilder.AddVault(options => { + // options.Address = vaultOptions["Address"]; - // options.UnsealKeys = vaultOptions.GetSection("UnsealKeys").Get>(); + // options.UnsealKeys = vaultOptions.GetSection("UnsealKeys").Get>(); - // options.AuthMethod = EnumerationStringId.FromValue(vaultOptions["AuthMethod"]); - // options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get(); - // options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get(); + // options.AuthMethod = EnumerationStringId.FromValue(vaultOptions["AuthMethod"]); + // options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get(); + // options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get(); - // options.MountPath = vaultOptions["MountPath"]; - // options.SecretType = vaultOptions["SecretType"]; + // options.MountPath = vaultOptions["MountPath"]; + // options.SecretType = vaultOptions["SecretType"]; - // options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get>(); - //}); + // options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get>(); + //}); - return configurationBuilder.Build(); - } + return configurationBuilder.Build(); } } diff --git a/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs b/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs index 7d52b27..b608520 100644 --- a/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs +++ b/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs @@ -4,27 +4,26 @@ using Serilog; using Microsoft.Extensions.Configuration; -using SSHProvider; +using MaksIT.SSHProvider; -namespace MaksIT.Tests.SSHProviderTests.Abstractions { - public abstract class ServicesBase : ConfigurationBase { +namespace MaksIT.Tests.SSHProviderTests.Abstractions; +public abstract class ServicesBase : ConfigurationBase { - public ServicesBase() : base() { } + public ServicesBase() : base() { } - protected override void ConfigureServices(IServiceCollection services) { - // configure strongly typed settings objects - var appSettingsSection = Configuration.GetSection("Configuration"); - services.Configure(appSettingsSection); - var appSettings = appSettingsSection.Get(); + protected override void ConfigureServices(IServiceCollection services) { + // configure strongly typed settings objects + var appSettingsSection = Configuration.GetSection("Configuration"); + services.Configure(appSettingsSection); + var appSettings = appSettingsSection.Get(); - #region configurazione logging - services.AddLogging(configure => { - configure.AddSerilog(new LoggerConfiguration() - //.ReadFrom.Configuration(_configuration) - .CreateLogger()); - }); - #endregion + #region configurazione logging + services.AddLogging(configure => { + configure.AddSerilog(new LoggerConfiguration() + //.ReadFrom.Configuration(_configuration) + .CreateLogger()); + }); + #endregion - } } } \ No newline at end of file diff --git a/src/Tests/SSHSerivceTests/SSHProviderTests.csproj b/src/Tests/SSHSerivceTests/SSHProviderTests.csproj index 49415f7..cb4a20a 100644 --- a/src/Tests/SSHSerivceTests/SSHProviderTests.csproj +++ b/src/Tests/SSHSerivceTests/SSHProviderTests.csproj @@ -10,13 +10,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Tests/SSHSerivceTests/UnitTest1.cs b/src/Tests/SSHSerivceTests/UnitTest1.cs index 6d87802..b4cfa22 100644 --- a/src/Tests/SSHSerivceTests/UnitTest1.cs +++ b/src/Tests/SSHSerivceTests/UnitTest1.cs @@ -3,54 +3,53 @@ using System.Security.Cryptography; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using SSHProvider; +using MaksIT.SSHProvider; using MaksIT.Tests.SSHProviderTests.Abstractions; -namespace SSHSerivceTests { - public class UnitTest1 : ServicesBase { +namespace MaksIT.SSHSerivceTests; +public class UnitTest1 : ServicesBase { - public readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; + public readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; - [Fact] - public void UploadFile() { + [Fact] + public void UploadFile() { - var username = ""; - var password = ""; - var filePath = Path.Combine(_appPath, "randomfile.txt"); - CreateRandomFile(filePath, 1); + var username = ""; + var password = ""; + var filePath = Path.Combine(_appPath, "randomfile.txt"); + CreateRandomFile(filePath, 1); - var logger = ServiceProvider.GetService>(); + var logger = ServiceProvider.GetService>(); - using var sshService = new SSHService(logger, "192.168.0.10", 22, username, password); - sshService.Connect(); + using var sshService = new SSHService(logger, "192.168.0.10", 22, username, password); + sshService.Connect(); - var bytes = File.ReadAllBytes(filePath); + var bytes = File.ReadAllBytes(filePath); - logger.LogInformation($"Uploading {filePath} ({bytes.Length:N0} bytes)"); + logger.LogInformation($"Uploading {filePath} ({bytes.Length:N0} bytes)"); - sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R"); - sshService.RunSudoCommand(password, "chmod 777 /var/www/ssl -R"); + sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R"); + sshService.RunSudoCommand(password, "chmod 777 /var/www/ssl -R"); - sshService.Upload("/var/www/ssl", Path.GetFileName(filePath), bytes); + sshService.Upload("/var/www/ssl", Path.GetFileName(filePath), bytes); - sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R"); - sshService.RunSudoCommand(password, "chmod 775 /var/www/ssl -R"); - } + sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R"); + sshService.RunSudoCommand(password, "chmod 775 /var/www/ssl -R"); + } - private void CreateRandomFile(string filePath, int sizeInMb) { - // Note: block size must be a factor of 1MB to avoid rounding errors - const int blockSize = 1024 * 8; - const int blocksPerMb = (1024 * 1024) / blockSize; + private void CreateRandomFile(string filePath, int sizeInMb) { + // Note: block size must be a factor of 1MB to avoid rounding errors + const int blockSize = 1024 * 8; + const int blocksPerMb = (1024 * 1024) / blockSize; - byte[] data = new byte[blockSize]; + byte[] data = new byte[blockSize]; - using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) { - using (FileStream stream = File.OpenWrite(filePath)) { - for (int i = 0; i < sizeInMb * blocksPerMb; i++) { - crypto.GetBytes(data); - stream.Write(data, 0, data.Length); - } + using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) { + using (FileStream stream = File.OpenWrite(filePath)) { + for (int i = 0; i < sizeInMb * blocksPerMb; i++) { + crypto.GetBytes(data); + stream.Write(data, 0, data.Length); } } }