(refactor): codebase review, small syntax fixes

This commit is contained in:
Maksym Sadovnychyy 2023-08-01 12:31:24 +02:00
parent 23fa0d9826
commit 767b4f2fc6
29 changed files with 1368 additions and 1410 deletions

View File

@ -8,7 +8,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Abstractions\" /> <Compile Remove="Abstractions\**" />
<EmbeddedResource Remove="Abstractions\**" />
<None Remove="Abstractions\**" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,37 +1,36 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.Json; using System.Text.Json;
namespace MaksIT.Core.Extensions { namespace MaksIT.Core.Extensions;
public static class ObjectExtensions { public static class ObjectExtensions {
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <param name="obj"></param> /// <param name="obj"></param>
/// <returns></returns> /// <returns></returns>
public static string ToJson<T>(this T? obj) => obj.ToJson(null); public static string ToJson<T>(this T? obj) => obj.ToJson(null);
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <param name="obj"></param> /// <param name="obj"></param>
/// <param name="converters"></param> /// <param name="converters"></param>
/// <returns></returns> /// <returns></returns>
public static string ToJson<T>(this T? obj, List<JsonConverter>? converters) { public static string ToJson<T>(this T? obj, List<JsonConverter>? converters) {
if (obj == null) if (obj == null)
return "{}"; return "{}";
var options = new JsonSerializerOptions { var options = new JsonSerializerOptions {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true 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);
}
} }
} }

View File

@ -6,35 +6,34 @@ using System.Text.Json.Serialization;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace MaksIT.Core.Extensions { namespace MaksIT.Core.Extensions;
public static class StringExtensions { public static class StringExtensions {
/// <summary> /// <summary>
/// Converts JSON string to object /// Converts JSON string to object
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <param name="s"></param> /// <param name="s"></param>
/// <returns></returns> /// <returns></returns>
public static T? ToObject<T>(this string? s) => ToObjectCore<T>(s, null); public static T? ToObject<T>(this string? s) => ToObjectCore<T>(s, null);
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <param name="s"></param> /// <param name="s"></param>
/// <param name="converters"></param> /// <param name="converters"></param>
/// <returns></returns> /// <returns></returns>
public static T? ToObject<T>(this string? s, List<JsonConverter> converters) => ToObjectCore<T>(s, converters); public static T? ToObject<T>(this string? s, List<JsonConverter> converters) => ToObjectCore<T>(s, converters);
private static T? ToObjectCore<T>(string? s, List<JsonConverter>? converters) { private static T? ToObjectCore<T>(string? s, List<JsonConverter>? converters) {
var options = new JsonSerializerOptions { var options = new JsonSerializerOptions {
PropertyNameCaseInsensitive = true PropertyNameCaseInsensitive = true
}; };
converters?.ForEach(x => options.Converters.Add(x)); converters?.ForEach(x => options.Converters.Add(x));
return s != null return s != null
? JsonSerializer.Deserialize<T>(s, options) ? JsonSerializer.Deserialize<T>(s, options)
: default; : default;
}
} }
} }

View File

@ -1,14 +1,13 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace MaksIT.Core { namespace MaksIT.Core;
public static class OperatingSystem { public static class OperatingSystem {
public static bool IsWindows() => public static bool IsWindows() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows); RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public static bool IsMacOS() => public static bool IsMacOS() =>
RuntimeInformation.IsOSPlatform(OSPlatform.OSX); RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public static bool IsLinux() => public static bool IsLinux() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Linux); RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
}
} }

View File

@ -2,104 +2,103 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace MaksIT.LetsEncrypt.Entities.Jws { namespace MaksIT.LetsEncrypt.Entities.Jws;
public class Jwk { public class Jwk {
/// <summary> /// <summary>
/// "kty" (Key Type) Parameter /// "kty" (Key Type) Parameter
/// <para> /// <para>
/// The "kty" (key type) parameter identifies the cryptographic algorithm /// The "kty" (key type) parameter identifies the cryptographic algorithm
/// family used with the key, such as "RSA" or "EC". /// family used with the key, such as "RSA" or "EC".
/// </para> /// </para>
/// </summary> /// </summary>
[JsonPropertyName("kty")] [JsonPropertyName("kty")]
public string? KeyType { get; set; } public string? KeyType { get; set; }
/// <summary> /// <summary>
/// "kid" (Key ID) Parameter /// "kid" (Key ID) Parameter
/// <para> /// <para>
/// The "kid" (key ID) parameter is used to match a specific key. This /// 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 /// 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 /// during key rollover. The structure of the "kid" value is
/// unspecified. /// unspecified.
/// </para> /// </para>
/// </summary> /// </summary>
[JsonPropertyName("kid")] [JsonPropertyName("kid")]
public string? KeyId { get; set; } public string? KeyId { get; set; }
/// <summary> /// <summary>
/// "use" (Public Key Use) Parameter /// "use" (Public Key Use) Parameter
/// <para> /// <para>
/// The "use" (public key use) parameter identifies the intended use of /// The "use" (public key use) parameter identifies the intended use of
/// the public key. The "use" parameter is employed to indicate whether /// the public key. The "use" parameter is employed to indicate whether
/// a public key is used for encrypting data or verifying the signature /// a public key is used for encrypting data or verifying the signature
/// on data. /// on data.
/// </para> /// </para>
/// </summary> /// </summary>
[JsonPropertyName("use")] [JsonPropertyName("use")]
public string? Use { get; set; } public string? Use { get; set; }
/// <summary> /// <summary>
/// The the modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. /// The the modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation.
/// </summary> /// </summary>
[JsonPropertyName("n")] [JsonPropertyName("n")]
public string? Modulus { get; set; } public string? Modulus { get; set; }
/// <summary> /// <summary>
/// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. /// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation.
/// </summary> /// </summary>
[JsonPropertyName("e")] [JsonPropertyName("e")]
public string? Exponent { get; set; } public string? Exponent { get; set; }
/// <summary> /// <summary>
/// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation. /// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary> /// </summary>
[JsonPropertyName("d")] [JsonPropertyName("d")]
public string? D { get; set; } public string? D { get; set; }
/// <summary> /// <summary>
/// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation. /// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary> /// </summary>
[JsonPropertyName("p")] [JsonPropertyName("p")]
public string? P { get; set; } public string? P { get; set; }
/// <summary> /// <summary>
/// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation. /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary> /// </summary>
[JsonPropertyName("q")] [JsonPropertyName("q")]
public string? Q { get; set; } public string? Q { get; set; }
/// <summary> /// <summary>
/// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. /// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary> /// </summary>
[JsonPropertyName("dp")] [JsonPropertyName("dp")]
public string? DP { get; set; } public string? DP { get; set; }
/// <summary> /// <summary>
/// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. /// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary> /// </summary>
[JsonPropertyName("dq")] [JsonPropertyName("dq")]
public string? DQ { get; set; } public string? DQ { get; set; }
/// <summary> /// <summary>
/// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation. /// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation.
/// </summary> /// </summary>
[JsonPropertyName("qi")] [JsonPropertyName("qi")]
public string? InverseQ { get; set; } public string? InverseQ { get; set; }
/// <summary> /// <summary>
/// The other primes information, should they exist, null or an empty list if not specified. /// The other primes information, should they exist, null or an empty list if not specified.
/// </summary> /// </summary>
[JsonPropertyName("oth")] [JsonPropertyName("oth")]
public string? OthInf { get; set; } public string? OthInf { get; set; }
/// <summary> /// <summary>
/// "alg" (Algorithm) Parameter /// "alg" (Algorithm) Parameter
/// <para> /// <para>
/// The "alg" (algorithm) parameter identifies the algorithm intended for /// The "alg" (algorithm) parameter identifies the algorithm intended for
/// use with the key. /// use with the key.
/// </para> /// </para>
/// </summary> /// </summary>
[JsonPropertyName("alg")] [JsonPropertyName("alg")]
public string? Algorithm { get; set; } public string? Algorithm { get; set; }
}
} }

View File

@ -1,40 +1,36 @@
using System; using System;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace MaksIT.LetsEncrypt.Entities.Jws namespace MaksIT.LetsEncrypt.Entities.Jws;
{
public class JwsMessage {
public string? Protected { get; set; }
public string? Payload { get; set; }
public string? Signature { get; set; }
}
public class JwsHeader { public class JwsMessage {
[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 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; }
} }

View File

@ -1,13 +1,12 @@
using System; using System;
namespace MaksIT.LetsEncrypt.Entities { namespace MaksIT.LetsEncrypt.Entities;
public class AuthorizationChallenge { public class AuthorizationChallenge {
public Uri? Url { get; set; } 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; }
}
} }

View File

@ -1,11 +1,8 @@
using System.Security.Cryptography; using System.Security.Cryptography;
namespace MaksIT.LetsEncrypt.Entities namespace MaksIT.LetsEncrypt.Entities;
{
public class CachedCertificateResult
{
public RSACryptoServiceProvider? PrivateKey { get; set; }
public string? Certificate { get; set; }
}
public class CachedCertificateResult {
public RSACryptoServiceProvider? PrivateKey { get; set; }
public string? Certificate { get; set; }
} }

View File

@ -1,18 +1,62 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.Text;
using MaksIT.LetsEncrypt.Entities.Jws; using MaksIT.LetsEncrypt.Entities.Jws;
namespace MaksIT.LetsEncrypt.Entities { namespace MaksIT.LetsEncrypt.Entities;
public class CertificateCache { public class CertificateCache {
public string? Cert { get; set; } public string? Cert { get; set; }
public byte[]? Private { get; set; } public byte[]? Private { get; set; }
}
public class RegistrationCache {
public Dictionary<string, CertificateCache>? CachedCerts { get; set; }
public byte[]? AccountKey { get; set; }
public string? Id { get; set; }
public Jwk? Key { get; set; }
public Uri? Location { get; set; }
/// <summary>
///
/// </summary>
/// <param name="subject"></param>
/// <param name="value"></param>
/// <returns></returns>
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 { /// <summary>
public Dictionary<string, CertificateCache>? CachedCerts { get; set; } ///
public byte[]? AccountKey { get; set; } /// </summary>
public string? Id { get; set; } /// <param name="hostsToRemove"></param>
public Jwk? Key { get; set; } public void ResetCachedCertificate(IEnumerable<string> hostsToRemove) {
public Uri? Location { get; set; } if (CachedCerts != null)
foreach (var host in hostsToRemove)
CachedCerts.Remove(host);
} }
} }

View File

@ -1,25 +1,23 @@
using System; 
using System.Net.Http; namespace MaksIT.LetsEncrypt.Exceptions;
public class LetsEncrytException : Exception {
namespace MaksIT.LetsEncrypt.Exceptions { public LetsEncrytException(Problem problem, HttpResponseMessage response)
public class LetsEncrytException : Exception { : base($"{problem.Type}: {problem.Detail}") {
public LetsEncrytException(Problem problem, HttpResponseMessage response) Problem = problem;
: base($"{problem.Type}: {problem.Detail}") { Response = response;
Problem = problem;
Response = response;
}
public Problem Problem { get; }
public HttpResponseMessage Response { get; }
} }
public Problem Problem { get; }
public class Problem { public HttpResponseMessage Response { get; }
public string Type { get; set; }
public string Detail { get; set; }
public string RawJson { get; set; }
}
} }
public class Problem {
public string Type { get; set; }
public string Detail { get; set; }
public string RawJson { get; set; }
}

View File

@ -2,11 +2,10 @@
using MaksIT.LetsEncrypt.Services; using MaksIT.LetsEncrypt.Services;
namespace MaksIT.LetsEncrypt.Extensions { namespace MaksIT.LetsEncrypt.Extensions;
public static class ServiceCollectionExtensions { public static class ServiceCollectionExtensions {
public static void RegisterLetsEncrypt(this IServiceCollection services) { public static void RegisterLetsEncrypt(this IServiceCollection services) {
services.AddHttpClient<ILetsEncryptService, LetsEncryptService>(); services.AddHttpClient<ILetsEncryptService, LetsEncryptService>();
}
} }
} }

View File

@ -1,5 +1,4 @@
namespace MaksIT.LetsEncrypt.Models.Interfaces { namespace MaksIT.LetsEncrypt.Models.Interfaces;
public interface IHasLocation { public interface IHasLocation {
Uri? Location { get; set; } Uri? Location { get; set; }
}
} }

View File

@ -1,7 +1,5 @@
namespace MaksIT.LetsEncrypt.Models.Requests namespace MaksIT.LetsEncrypt.Models.Requests;
{
public class FinalizeRequest public class FinalizeRequest {
{ public string? Csr { get; set; }
public string? Csr { get; set; }
}
} }

View File

@ -5,34 +5,33 @@ using MaksIT.LetsEncrypt.Models.Interfaces;
* https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-7.3 * https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-7.3
*/ */
namespace MaksIT.LetsEncrypt.Models.Responses namespace MaksIT.LetsEncrypt.Models.Responses;
{
public class Account : IHasLocation {
public bool TermsOfServiceAgreed { get; set; } public class Account : IHasLocation {
/* public bool TermsOfServiceAgreed { 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[]? 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; }
} }

View File

@ -1,29 +1,25 @@
using System; using System;
namespace MaksIT.LetsEncrypt.Models.Responses namespace MaksIT.LetsEncrypt.Models.Responses;
{
public class AcmeDirectory
{
public Uri NewNonce { get; set; }
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 // New authorization If the ACME server does not implement pre-authorization
// (Section 7.4.1) it MUST omit the "newAuthz" field of the directory. // (Section 7.4.1) it MUST omit the "newAuthz" field of the directory.
// [JsonProperty("newAuthz")] // [JsonProperty("newAuthz")]
// public Uri NewAuthz { get; set; } // public Uri NewAuthz { get; set; }
public Uri RevokeCertificate { get; set; } public Uri RevokeCertificate { get; set; }
public Uri KeyChange { get; set; } public Uri KeyChange { get; set; }
public AcmeDirectoryMeta Meta { get; set; } public AcmeDirectoryMeta Meta { get; set; }
} }
public class AcmeDirectoryMeta public class AcmeDirectoryMeta {
{ public string TermsOfService { get; set; }
public string TermsOfService { get; set; }
}
} }

View File

@ -1,20 +1,20 @@
 
using MaksIT.LetsEncrypt.Entities; using MaksIT.LetsEncrypt.Entities;
namespace MaksIT.LetsEncrypt.Models.Responses { namespace MaksIT.LetsEncrypt.Models.Responses;
public class AuthorizationChallengeResponse {
public OrderIdentifier? Identifier { get; set; }
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 AuthorizationChallenge[]? Challenges { get; set; }
public string? KeyAuthorization { get; set; } }
}
public class AuthorizeChallenge {
public string? KeyAuthorization { get; set; }
} }

View File

@ -1,34 +1,33 @@
using MaksIT.LetsEncrypt.Exceptions; using MaksIT.LetsEncrypt.Exceptions;
using MaksIT.LetsEncrypt.Models.Interfaces; using MaksIT.LetsEncrypt.Models.Interfaces;
namespace MaksIT.LetsEncrypt.Models.Responses { namespace MaksIT.LetsEncrypt.Models.Responses;
public class OrderIdentifier { public class OrderIdentifier {
public string? Type { get; set; } public string? Type { get; set; }
public string? Value { get; set; } public string? Value { get; set; }
} }
public class Order : IHasLocation { public class Order : IHasLocation {
public Uri? Location { get; set; } public Uri? Location { get; set; }
public string? Status { get; set; } public string? Status { get; set; }
public DateTime? Expires { get; set; } public DateTime? Expires { get; set; }
public OrderIdentifier[]? Identifiers { get; set; } public OrderIdentifier[]? Identifiers { get; set; }
public DateTime? NotBefore { get; set; } public DateTime? NotBefore { get; set; }
public DateTime? NotAfter { get; set; } public DateTime? NotAfter { get; set; }
public Problem? Error { get; set; } public Problem? Error { get; set; }
public Uri[]? Authorizations { get; set; } public Uri[]? Authorizations { get; set; }
public Uri? Finalize { get; set; } public Uri? Finalize { get; set; }
public Uri? Certificate { get; set; } public Uri? Certificate { get; set; }
}
} }

View File

@ -11,102 +11,102 @@ using MaksIT.LetsEncrypt.Entities.Jws;
using MaksIT.Core.Extensions; using MaksIT.Core.Extensions;
namespace MaksIT.LetsEncrypt.Services { namespace MaksIT.LetsEncrypt.Services;
public interface IJwsService {
void SetKeyId(string location);
JwsMessage Encode(JwsHeader protectedHeader); public interface IJwsService {
void SetKeyId(string location);
JwsMessage Encode<TPayload>(TPayload payload, JwsHeader protectedHeader); JwsMessage Encode(JwsHeader protectedHeader);
string GetKeyAuthorization(string token); JwsMessage Encode<TPayload>(TPayload payload, JwsHeader protectedHeader);
string GetKeyAuthorization(string token);
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<string>(null, protectedHeader);
public JwsMessage Encode<T>(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(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<string>(null, protectedHeader);
public JwsMessage Encode<T>(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
}
} }

View File

@ -18,460 +18,457 @@ using MaksIT.LetsEncrypt.Models.Responses;
using MaksIT.LetsEncrypt.Models.Interfaces; using MaksIT.LetsEncrypt.Models.Interfaces;
using MaksIT.LetsEncrypt.Models.Requests; using MaksIT.LetsEncrypt.Models.Requests;
using MaksIT.LetsEncrypt.Entities.Jws; using MaksIT.LetsEncrypt.Entities.Jws;
using System.Xml;
using System.Diagnostics;
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(); string GetTermsOfServiceUri();
Task<Dictionary<string, string>> NewOrder(string[] hostnames, string challengeType); Task<Dictionary<string, string>> NewOrder(string[] hostnames, string challengeType);
Task CompleteChallenges(); Task CompleteChallenges();
Task GetOrder(string[] hostnames); Task GetOrder(string[] hostnames);
Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject); Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject);
RegistrationCache? GetRegistrationCache(); RegistrationCache? GetRegistrationCache();
}
public class LetsEncryptService : ILetsEncryptService {
//private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings {
// NullValueHandling = NullValueHandling.Ignore,
// Formatting = Formatting.Indented
//};
private readonly ILogger<LetsEncryptService> _logger;
private HttpClient _httpClient;
private IJwsService _jwsService;
private AcmeDirectory? _directory;
private RegistrationCache? _cache;
private string? _nonce;
private List<AuthorizationChallenge> _challenges = new List<AuthorizationChallenge>();
private Order? _currentOrder;
public LetsEncryptService(
ILogger<LetsEncryptService> logger,
HttpClient httpClient
) {
_logger = logger;
_httpClient = httpClient;
}
/// <summary>
///
/// </summary>
/// <param name="url"></param>
/// <param name="contacts"></param>
/// <returns></returns>
public async Task ConfigureClient(string url) {
_httpClient.BaseAddress ??= new Uri(url);
(_directory, _) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null);
}
/// <summary>
/// Account creation or Initialization from cache
/// </summary>
/// <param name="contacts"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task Init(string? [] contacts, RegistrationCache? cache) {
if (contacts == null || contacts.Length == 0)
throw new ArgumentNullException();
if (_directory == null)
throw new ArgumentNullException();
var accountKey = new RSACryptoServiceProvider(4096);
if (cache != null && cache.AccountKey != null) {
_cache = cache;
accountKey.ImportCspBlob(cache.AccountKey);
}
// New Account request
_jwsService = new JwsService(accountKey);
var letsEncryptOrder = new Account {
TermsOfServiceAgreed = true,
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
};
var (account, response) = await SendAsync<Account>(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder);
_jwsService.SetKeyId(account.Location.ToString());
if (account.Status != "valid")
throw new InvalidOperationException($"Account status is not valid, was: {account.Status} \r\n {response}");
_cache = new RegistrationCache {
Location = account.Location,
AccountKey = accountKey.ExportCspBlob(true),
Id = account.Id,
Key = account.Key
};
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public RegistrationCache? GetRegistrationCache() =>
_cache;
/// <summary>
/// Just retrive terms of service
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public string GetTermsOfServiceUri() {
if (_directory == null)
throw new NullReferenceException();
return _directory.Meta.TermsOfService;
} }
/// <summary>
public class LetsEncryptService : ILetsEncryptService { /// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange.
/// <para>
//private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings { /// Available challange types:
// NullValueHandling = NullValueHandling.Ignore, /// <list type="number">
// Formatting = Formatting.Indented /// <item>dns-01</item>
//}; /// <item>http-01</item>
/// <item>tls-alpn-01</item>
private readonly ILogger<LetsEncryptService> _logger; /// </list>
/// </para>
private HttpClient _httpClient; /// </summary>
/// <param name="hostnames"></param>
private IJwsService _jwsService; /// <param name="challengeType"></param>
private AcmeDirectory? _directory; /// <param name="token"></param>
private RegistrationCache? _cache; /// <returns></returns>
public async Task<Dictionary<string, string>> NewOrder(string[] hostnames, string challengeType) {
private string? _nonce; _challenges.Clear();
private List<AuthorizationChallenge> _challenges = new List<AuthorizationChallenge>(); var letsEncryptOrder = new Order {
private Order? _currentOrder; Expires = DateTime.UtcNow.AddDays(2),
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
public LetsEncryptService( Type = "dns",
ILogger<LetsEncryptService> logger, Value = hostname
HttpClient httpClient }).ToArray()
) { };
_logger = logger;
_httpClient = httpClient; var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
}
if (order.Status == "ready")
return new Dictionary<string, string>();
/// <summary>
/// if (order.Status != "pending")
/// </summary> throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}");
/// <param name="url"></param>
/// <param name="contacts"></param> _currentOrder = order;
/// <returns></returns>
public async Task ConfigureClient(string url) { var results = new Dictionary<string, string>();
foreach (var item in order.Authorizations) {
_httpClient.BaseAddress ??= new Uri(url); var (challengeResponse, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, true, null);
(_directory, _) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null); if (challengeResponse.Status == "valid")
}
/// <summary>
/// Account creation or Initialization from cache
/// </summary>
/// <param name="contacts"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task Init(string? [] contacts, RegistrationCache? cache) {
if (contacts == null || contacts.Length == 0)
throw new ArgumentNullException();
if (_directory == null)
throw new ArgumentNullException();
var accountKey = new RSACryptoServiceProvider(4096);
if (cache != null && cache.AccountKey != null) {
_cache = cache;
accountKey.ImportCspBlob(cache.AccountKey);
}
// New Account request
_jwsService = new JwsService(accountKey);
var letsEncryptOrder = new Account {
TermsOfServiceAgreed = true,
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
};
var (account, response) = await SendAsync<Account>(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder);
_jwsService.SetKeyId(account.Location.ToString());
if (account.Status != "valid")
throw new InvalidOperationException($"Account status is not valid, was: {account.Status} \r\n {response}");
_cache = new RegistrationCache {
Location = account.Location,
AccountKey = accountKey.ExportCspBlob(true),
Id = account.Id,
Key = account.Key
};
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public RegistrationCache? GetRegistrationCache() =>
_cache;
/// <summary>
/// Just retrive terms of service
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public string GetTermsOfServiceUri() {
if (_directory == null)
throw new NullReferenceException();
return _directory.Meta.TermsOfService;
}
/// <summary>
/// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange.
/// <para>
/// Available challange types:
/// <list type="number">
/// <item>dns-01</item>
/// <item>http-01</item>
/// <item>tls-alpn-01</item>
/// </list>
/// </para>
/// </summary>
/// <param name="hostnames"></param>
/// <param name="challengeType"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<Dictionary<string, string>> NewOrder(string[] hostnames, string challengeType) {
_challenges.Clear();
var letsEncryptOrder = new Order {
Expires = DateTime.UtcNow.AddDays(2),
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
Type = "dns",
Value = hostname
}).ToArray()
};
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
if (order.Status == "ready")
return new Dictionary<string, string>();
if (order.Status != "pending")
throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}");
_currentOrder = order;
var results = new Dictionary<string, string>();
foreach (var item in order.Authorizations) {
var (challengeResponse, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, true, null);
if (challengeResponse.Status == "valid")
continue;
if (challengeResponse.Status != "pending")
throw new InvalidOperationException($"Expected autorization status 'pending', but got: {order.Status} \r\n {responseText}");
var challenge = challengeResponse.Challenges.First(x => x.Type == challengeType);
_challenges.Add(challenge);
var keyToken = _jwsService.GetKeyAuthorization(challenge.Token);
switch (challengeType) {
// A client fulfills this challenge by constructing a key authorization
// from the "token" value provided in the challenge and the client's
// account key. The client then computes the SHA-256 digest [FIPS180-4]
// of the key authorization.
//
// The record provisioned to the DNS contains the base64url encoding of
// this digest.
case "dns-01": {
using (var sha256 = SHA256.Create()) {
var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
results[challengeResponse.Identifier.Value] = dnsToken;
}
break;
}
// A client fulfills this challenge by constructing a key authorization
// from the "token" value provided in the challenge and the client's
// account key. The client then provisions the key authorization as a
// resource on the HTTP server for the domain in question.
//
// The path at which the resource is provisioned is comprised of the
// fixed prefix "/.well-known/acme-challenge/", followed by the "token"
// value in the challenge. The value of the resource MUST be the ASCII
// representation of the key authorization.
case "http-01": {
results[challengeResponse.Identifier.Value] = keyToken;
break;
}
default:
throw new NotImplementedException();
}
}
return results;
}
/// <summary>
///
/// </summary>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task CompleteChallenges() {
for (var index = 0; index < _challenges.Count; index++) {
var challenge = _challenges[index];
while (true) {
AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge();
switch (challenge.Type) {
case "dns-01": {
authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token);
//var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, authorizeChallenge, token);
break;
}
case "http-01": {
break;
}
}
var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}");
if (result.Status == "valid")
break;
if (result.Status != "pending")
throw new InvalidOperationException($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {responseText}");
await Task.Delay(1000);
}
}
}
/// <summary>
///
/// </summary>
/// <param name="hostnames"></param>
/// <returns></returns>
public async Task GetOrder(string[] hostnames) {
var letsEncryptOrder = new Order {
Expires = DateTime.UtcNow.AddDays(2),
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
Type = "dns",
Value = hostname
}).ToArray()
};
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
_currentOrder = order;
}
/// <summary>
///
/// </summary>
/// <param name="subject"></param>
/// <returns>Cert and Private key</returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) {
_logger.LogInformation($"Invoked: {nameof(GetCertificate)}");
if (_currentOrder == null)
throw new ArgumentNullException();
var key = new RSACryptoServiceProvider(4096);
var csr = new CertificateRequest("CN=" + subject,
key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var san = new SubjectAlternativeNameBuilder();
foreach (var host in _currentOrder.Identifiers)
san.AddDnsName(host.Value);
csr.CertificateExtensions.Add(san.Build());
var letsEncryptOrder = new FinalizeRequest {
Csr = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest())
};
Uri? certificateUrl = default;
var start = DateTime.UtcNow;
while (certificateUrl == null) {
// https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882
await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray());
if (_currentOrder.Status == "ready") {
var (response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder);
if (response.Status == "processing")
(response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Location, true, null);
if (response.Status == "valid") {
certificateUrl = response.Certificate;
}
}
if ((start - DateTime.UtcNow).Seconds > 120)
throw new TimeoutException();
await Task.Delay(1000);
continue; continue;
// throw new InvalidOperationException(/*$"Invalid order status: "*/); if (challengeResponse.Status != "pending")
throw new InvalidOperationException($"Expected autorization status 'pending', but got: {order.Status} \r\n {responseText}");
var challenge = challengeResponse.Challenges.First(x => x.Type == challengeType);
_challenges.Add(challenge);
var keyToken = _jwsService.GetKeyAuthorization(challenge.Token);
switch (challengeType) {
// A client fulfills this challenge by constructing a key authorization
// from the "token" value provided in the challenge and the client's
// account key. The client then computes the SHA-256 digest [FIPS180-4]
// of the key authorization.
//
// The record provisioned to the DNS contains the base64url encoding of
// this digest.
case "dns-01": {
using (var sha256 = SHA256.Create()) {
var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
results[challengeResponse.Identifier.Value] = dnsToken;
}
break;
}
// A client fulfills this challenge by constructing a key authorization
// from the "token" value provided in the challenge and the client's
// account key. The client then provisions the key authorization as a
// resource on the HTTP server for the domain in question.
//
// The path at which the resource is provisioned is comprised of the
// fixed prefix "/.well-known/acme-challenge/", followed by the "token"
// value in the challenge. The value of the resource MUST be the ASCII
// representation of the key authorization.
case "http-01": {
results[challengeResponse.Identifier.Value] = keyToken;
break;
}
default:
throw new NotImplementedException();
} }
var (pem, _) = await SendAsync<string>(HttpMethod.Post, certificateUrl, true, null);
if (_cache == null)
throw new NullReferenceException();
_cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
_cache.CachedCerts[subject] = new CertificateCache {
Cert = pem,
Private = key.ExportCspBlob(true)
};
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem));
return (cert, key);
} }
/// <summary> return results;
/// }
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public Task KeyChange() {
throw new NotImplementedException();
}
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="token"></param> /// <returns></returns>
/// <returns></returns> /// <exception cref="InvalidOperationException"></exception>
public Task RevokeCertificate() { public async Task CompleteChallenges() {
throw new NotImplementedException();
}
/// <summary> for (var index = 0; index < _challenges.Count; index++) {
/// Main method used to send data to LetsEncrypt
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="method"></param>
/// <param name="uri"></param>
/// <param name="message"></param>
/// <param name="token"></param>
/// <returns></returns>
private async Task<(TResult, string)> SendAsync<TResult>(HttpMethod method, Uri uri, bool isPostAsGet, object? message) where TResult : class {
var request = new HttpRequestMessage(method, uri);
_nonce = uri.OriginalString != "directory" var challenge = _challenges[index];
? await NewNonce()
: default;
if (message != null || isPostAsGet) { while (true) {
var jwsHeader = new JwsHeader { AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge();
Url = uri,
};
if (_nonce != null) switch (challenge.Type) {
jwsHeader.Nonce = _nonce; case "dns-01": {
authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token);
//var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, authorizeChallenge, token);
break;
}
var encodedMessage = isPostAsGet case "http-01": {
? _jwsService.Encode(jwsHeader) break;
: _jwsService.Encode(message, jwsHeader); }
}
var json = encodedMessage.ToJson(); var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}");
request.Content = new StringContent(json); if (result.Status == "valid")
break;
if (result.Status != "pending")
throw new InvalidOperationException($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {responseText}");
var requestType = "application/json"; await Task.Delay(1000);
if (method == HttpMethod.Post)
requestType = "application/jose+json";
request.Content.Headers.Remove("Content-Type");
request.Content.Headers.Add("Content-Type", requestType);
} }
var response = await _httpClient.SendAsync(request);
if (method == HttpMethod.Post)
_nonce = response.Headers.GetValues("Replay-Nonce").First();
if (response.Content.Headers.ContentType.MediaType == "application/problem+json") {
var problemJson = await response.Content.ReadAsStringAsync();
var problem = problemJson.ToObject<Problem>();
problem.RawJson = problemJson;
throw new LetsEncrytException(problem, response);
}
var responseText = await response.Content.ReadAsStringAsync();
if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") {
return ((TResult)(object)responseText, null);
}
var responseContent = responseText.ToObject<TResult>();
if (responseContent is IHasLocation ihl) {
if (response.Headers.Location != null)
ihl.Location = response.Headers.Location;
}
return (responseContent, responseText);
}
/// <summary>
/// Request New Nonce to be able to start POST requests
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
private async Task<string> NewNonce() {
if (_directory == null)
throw new NotImplementedException();
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce));
return result.Headers.GetValues("Replay-Nonce").First();
} }
} }
/// <summary>
///
/// </summary>
/// <param name="hostnames"></param>
/// <returns></returns>
public async Task GetOrder(string[] hostnames) {
var letsEncryptOrder = new Order {
Expires = DateTime.UtcNow.AddDays(2),
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
Type = "dns",
Value = hostname
}).ToArray()
};
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
_currentOrder = order;
}
/// <summary>
///
/// </summary>
/// <param name="subject"></param>
/// <returns>Cert and Private key</returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) {
_logger.LogInformation($"Invoked: {nameof(GetCertificate)}");
if (_currentOrder == null)
throw new ArgumentNullException();
var key = new RSACryptoServiceProvider(4096);
var csr = new CertificateRequest("CN=" + subject,
key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var san = new SubjectAlternativeNameBuilder();
foreach (var host in _currentOrder.Identifiers)
san.AddDnsName(host.Value);
csr.CertificateExtensions.Add(san.Build());
var letsEncryptOrder = new FinalizeRequest {
Csr = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest())
};
Uri? certificateUrl = default;
var start = DateTime.UtcNow;
while (certificateUrl == null) {
// https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882
await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray());
if (_currentOrder.Status == "ready") {
var (response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder);
if (response.Status == "processing")
(response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Location, true, null);
if (response.Status == "valid") {
certificateUrl = response.Certificate;
}
}
if ((start - DateTime.UtcNow).Seconds > 120)
throw new TimeoutException();
await Task.Delay(1000);
continue;
// throw new InvalidOperationException(/*$"Invalid order status: "*/);
}
var (pem, _) = await SendAsync<string>(HttpMethod.Post, certificateUrl, true, null);
if (_cache == null)
throw new NullReferenceException();
_cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
_cache.CachedCerts[subject] = new CertificateCache {
Cert = pem,
Private = key.ExportCspBlob(true)
};
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem));
return (cert, key);
}
/// <summary>
///
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public Task KeyChange() {
throw new NotImplementedException();
}
/// <summary>
///
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public Task RevokeCertificate() {
throw new NotImplementedException();
}
/// <summary>
/// Main method used to send data to LetsEncrypt
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="method"></param>
/// <param name="uri"></param>
/// <param name="message"></param>
/// <param name="token"></param>
/// <returns></returns>
private async Task<(TResult, string)> SendAsync<TResult>(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 (message != null || isPostAsGet) {
var jwsHeader = new JwsHeader {
Url = uri,
};
if (_nonce != null)
jwsHeader.Nonce = _nonce;
var encodedMessage = isPostAsGet
? _jwsService.Encode(jwsHeader)
: _jwsService.Encode(message, jwsHeader);
var json = encodedMessage.ToJson();
request.Content = new StringContent(json);
var requestType = "application/json";
if (method == HttpMethod.Post)
requestType = "application/jose+json";
request.Content.Headers.Remove("Content-Type");
request.Content.Headers.Add("Content-Type", requestType);
}
var response = await _httpClient.SendAsync(request);
if (method == HttpMethod.Post)
_nonce = response.Headers.GetValues("Replay-Nonce").First();
if (response.Content.Headers.ContentType.MediaType == "application/problem+json") {
var problemJson = await response.Content.ReadAsStringAsync();
var problem = problemJson.ToObject<Problem>();
problem.RawJson = problemJson;
throw new LetsEncrytException(problem, response);
}
var responseText = await response.Content.ReadAsStringAsync();
if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") {
return ((TResult)(object)responseText, null);
}
var responseContent = responseText.ToObject<TResult>();
if (responseContent is IHasLocation ihl) {
if (response.Headers.Location != null)
ihl.Location = response.Headers.Location;
}
return (responseContent, responseText);
}
/// <summary>
/// Request New Nonce to be able to start POST requests
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
private async Task<string> NewNonce() {
if (_directory == null)
throw new NotImplementedException();
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce));
return result.Headers.GetValues("Replay-Nonce").First();
}
} }

View File

@ -1,7 +1,3 @@
using System.Text;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -10,314 +6,268 @@ using MaksIT.Core.Extensions;
using MaksIT.LetsEncrypt.Services; using MaksIT.LetsEncrypt.Services;
using MaksIT.LetsEncrypt.Entities; using MaksIT.LetsEncrypt.Entities;
using MaksIT.LetsEncryptConsole.Services; 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<App> _logger;
private readonly Configuration _appSettings;
private readonly ILetsEncryptService _letsEncryptService;
private readonly IKeyService _keyService;
private readonly ITerminalService _terminalService;
public App(
ILogger<App> logger,
IOptions<Configuration> appSettings,
ILetsEncryptService letsEncryptService,
IKeyService keyService,
ITerminalService terminalService
) {
_logger = logger;
_appSettings = appSettings.Value;
_letsEncryptService = letsEncryptService;
_keyService = keyService;
_terminalService = terminalService;
} }
public class App : IApp { public async Task Run(string[] args) {
private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; _logger.LogInformation("Letsencrypt client estarted...");
private readonly ILogger<App> _logger; foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List<LetsEncryptEnvironment>()) {
private readonly Configuration _appSettings; try {
private readonly ILetsEncryptService _letsEncryptService; _logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}");
private readonly IKeyService _keyService;
private readonly ITerminalService _terminalService;
public App( //loop all customers
ILogger<App> logger, foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List<Customer>()) {
IOptions<Configuration> appSettings, try {
ILetsEncryptService letsEncryptService, _logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}");
IKeyService keyService,
ITerminalService terminalService
) {
_logger = logger;
_appSettings = appSettings.Value;
_letsEncryptService = letsEncryptService;
_keyService = keyService;
_terminalService = terminalService;
}
public async Task Run(string[] args) { //define cache folder
string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache");
if (!Directory.Exists(cachePath)) {
Directory.CreateDirectory(cachePath);
}
_logger.LogInformation("Letsencrypt client estarted..."); //check acme directory
var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme");
if (!Directory.Exists(acmePath)) {
Directory.CreateDirectory(acmePath);
}
foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List<LetsEncryptEnvironment>()) { //loop each customer website
try { foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List<Site>()) {
_logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}"); _logger.LogInformation($"Managing site: {site.Name}");
//loop all customers try {
foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List<Customer>()) { //create folder for ssl
try { string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name);
_logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}"); if (!Directory.Exists(sslPath)) {
Directory.CreateDirectory(sslPath);
}
//define cache folder var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json");
string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache");
if (!Directory.Exists(cachePath)) {
Directory.CreateDirectory(cachePath);
}
//check acme directory //1. Client initialization
var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme"); _logger.LogInformation("1. Client Initialization...");
if (!Directory.Exists(acmePath)) {
Directory.CreateDirectory(acmePath);
}
//loop each customer website #region LetsEncrypt client configuration
foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List<Site>()) { await _letsEncryptService.ConfigureClient(env.Url);
_logger.LogInformation($"Managing site: {site.Name}"); #endregion
try { #region LetsEncrypt local registration cache initialization
//create folder for ssl var registrationCache = (File.Exists(cacheFile)
string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name); ? File.ReadAllText(cacheFile)
if (!Directory.Exists(sslPath)) { : null)
Directory.CreateDirectory(sslPath); .ToObject<RegistrationCache>();
}
var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json"); await _letsEncryptService.Init(customer.Contacts, registrationCache);
#endregion
//1. Client initialization #region LetsEncrypt terms of service
_logger.LogInformation("1. Client Initialization..."); _logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}");
#endregion
#region LetsEncrypt client configuration // get cached certificate and check if it's valid
await _letsEncryptService.ConfigureClient(env.Url); // if valid check if cert and key exists otherwise recreate
#endregion // else continue with new certificate request
var certRes = new CachedCertificateResult();
if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) {
string cert = Path.Combine(sslPath, $"{site.Name}.crt");
//if(!File.Exists(cert))
File.WriteAllText(cert, certRes.Certificate);
#region LetsEncrypt local registration cache initialization string key = Path.Combine(sslPath, $"{site.Name}.key");
var registrationCache = (File.Exists(cacheFile) //if(!File.Exists(key)) {
? File.ReadAllText(cacheFile) using (StreamWriter writer = File.CreateText(key))
: null) _keyService.ExportPrivateKey(certRes.PrivateKey, writer);
.ToObject<RegistrationCache>(); //}
await _letsEncryptService.Init(customer.Contacts, registrationCache); _logger.LogInformation("Certificate and Key exists and valid. Restored from cache.");
#endregion }
else {
#region LetsEncrypt terms of service //try to make new order
_logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}"); try {
#endregion //create new orders
Console.WriteLine("2. Client New Order...");
// get cached certificate and check if it's valid #region LetsEncrypt new order
// if valid check if cert and key exists otherwise recreate var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge);
// else continue with new certificate request #endregion
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 (orders.Count > 0) {
//if(!File.Exists(key)) { switch (site.Challenge) {
using (StreamWriter writer = File.CreateText(key)) case "http-01": {
_keyService.ExportPrivateKey(certRes.PrivateKey, writer); //ensure to enable static file discovery on server in .well-known/acme-challenge
//} //and listen on 80 port
_logger.LogInformation("Certificate and Key exists and valid. Restored from cache."); foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles())
} file.Delete();
else {
//try to make new order foreach (var result in orders) {
try { Console.WriteLine($"Key: {result.Key}, Value: {result.Value}");
//create new orders string[] splitToken = result.Value.Split('.');
Console.WriteLine("2. Client New Order...");
#region LetsEncrypt new order File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value);
var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge); }
#endregion
if (orders.Count > 0) { foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) {
switch (site.Challenge) { if (env?.SSH?.Active ?? false) {
case "http-01": { UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode);
//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);
} }
else {
foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) { throw new NotImplementedException();
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": { break;
//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 {
case "dns-01": {
//Manage DNS server MX record, depends from provider
throw new NotImplementedException(); throw new NotImplementedException();
} }
}
default: {
throw new NotImplementedException();
}
} }
else {
_logger.LogError("Unable to get new cached certificate.");
} #region LetsEncrypt complete challenges
_logger.LogInformation("3. Client Complete Challange...");
await _letsEncryptService.CompleteChallenges();
_logger.LogInformation("Challanges comleted.");
#endregion
await Task.Delay(1000);
#region Download new certificate
_logger.LogInformation("4. Download certificate...");
var (cert, key) = await _letsEncryptService.GetCertificate(site.Name);
#endregion
#region Persist cache
registrationCache = _letsEncryptService.GetRegistrationCache();
File.WriteAllText(cacheFile, registrationCache.ToJson());
#endregion #endregion
} }
catch (Exception ex) {
_logger.LogError(ex, "");
await _letsEncryptService.GetOrder(site.Hosts);
}
#region Save cert and key to filesystem
certRes = new CachedCertificateResult();
if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) {
File.WriteAllText(Path.Combine(sslPath, site.Name + ".crt"), certRes.Certificate);
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);
} }
}
catch (Exception ex) {
_logger.LogError(ex, "Customer unhandled error");
} }
}
catch (Exception ex) {
_logger.LogError(ex, "Customer unhandled error");
} }
} }
catch (Exception ex) {
_logger.LogError(ex, "Environment unhandled error");
}
} }
catch (Exception ex) {
if (env.Name == "ProductionV2") { _logger.LogError(ex, "Environment unhandled error");
_terminalService.Exec("systemctl restart nginx");
} }
} }
catch (Exception ex) {
_logger.LogError(ex.Message.ToString()); if (env.Name == "ProductionV2") {
break; _terminalService.Exec("systemctl restart nginx");
} }
} }
} catch (Exception ex) {
_logger.LogError(ex.Message.ToString());
/// <summary> break;
///
/// </summary>
/// <param name="subject"></param>
/// <param name="value"></param>
/// <returns></returns>
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;
}
/// <summary>
///
/// </summary>
/// <param name="hostsToRemove"></param>
public RegistrationCache? ResetCachedCertificate(RegistrationCache? registrationCache, IEnumerable<string> 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");
sshService.Upload($"{workDir}", fileName, bytes);
sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R");
sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {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.RunSudoCommand(sshSettings.Password, $"mkdir {workDir}");
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");
}
} }

View File

@ -1,40 +1,40 @@
using System.Runtime.InteropServices;
namespace MaksIT.LetsEncryptConsole { namespace MaksIT.LetsEncryptConsole;
public class Configuration {
public LetsEncryptEnvironment[]? Environments { get; set; }
public Customer[]? Customers { get; set; }
}
public class OsWindows { public class Configuration {
public string? Path { get; set; } public LetsEncryptEnvironment[]? Environments { get; set; }
} public Customer[]? Customers { get; set; }
}
public class OsLinux { public class OsWindows {
public string? Path { get; set; } 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 class OsDependant {
public bool Active { get; set; } 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[]? Hosts { get; set; }
public string? Challenge { get; set; } public string? Challenge { get; set; }
} }
}

View File

@ -6,65 +6,66 @@ using Serilog;
using MaksIT.LetsEncryptConsole.Services; using MaksIT.LetsEncryptConsole.Services;
using MaksIT.LetsEncrypt.Extensions; using MaksIT.LetsEncrypt.Extensions;
namespace MaksIT.LetsEncryptConsole { namespace MaksIT.LetsEncryptConsole;
class Program {
private static readonly IConfiguration _configuration = InitConfig();
static void Main(string[] args) { class Program {
// create service collection private static readonly IConfiguration _configuration = InitConfig();
var services = new ServiceCollection();
ConfigureServices(services);
// create service provider static void Main(string[] args) {
var serviceProvider = services.BuildServiceProvider(); // 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. #pragma warning disable CS8602 // Dereference of a possibly null reference.
var app = serviceProvider.GetService<App>(); var app = serviceProvider.GetService<App>();
app.Run(args).Wait(); app.Run(args).Wait();
#pragma warning restore CS8602 // Dereference of a possibly null reference. #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"); var configurationSection = _configuration.GetSection("Configuration");
services.Configure<Configuration>(configurationSection); services.Configure<Configuration>(configurationSection);
var appSettings = configurationSection.Get<Configuration>(); var appSettings = configurationSection.Get<Configuration>();
#region Configure logging #region Configure logging
services.AddLogging(configure => { services.AddLogging(configure => {
configure.AddSerilog(new LoggerConfiguration() configure.AddSerilog(new LoggerConfiguration()
.ReadFrom.Configuration(_configuration) .ReadFrom.Configuration(_configuration)
.CreateLogger()); .CreateLogger());
}); });
#endregion #endregion
#region Services #region Services
services.RegisterLetsEncrypt(); services.RegisterLetsEncrypt();
services.AddSingleton<IKeyService, KeyService>(); services.AddSingleton<IKeyService, KeyService>();
services.AddSingleton<ITerminalService, TerminalService>(); services.AddSingleton<ITerminalService, TerminalService>();
#endregion #endregion
// add app // add app
services.AddSingleton<App>(); services.AddSingleton<App>();
} }
private static IConfiguration InitConfig() { private static IConfiguration InitConfig() {
var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory()) .SetBasePath(Directory.GetCurrentDirectory())
.AddEnvironmentVariables(); .AddEnvironmentVariables();
if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment)
&& new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), $"appsettings.{aspNetCoreEnvironment}.json")).Exists && new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), $"appsettings.{aspNetCoreEnvironment}.json")).Exists
) )
configuration.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true); configuration.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true);
else else
configuration.AddJsonFile($"appsettings.json", true, true); configuration.AddJsonFile($"appsettings.json", true, true);
return configuration.Build(); return configuration.Build();
}
} }
} }

View File

@ -1,150 +1,149 @@
using System.Security.Cryptography; using System.Security.Cryptography;
namespace MaksIT.LetsEncryptConsole.Services { namespace MaksIT.LetsEncryptConsole.Services;
public interface IKeyService { public interface IKeyService {
void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream); void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream);
void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream); void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream);
}
public class KeyService : IKeyService {
/// <summary>
/// Export a certificate to a PEM format string
/// </summary>
/// <param name="cert">The certificate to export</param>
/// <returns>A PEM encoded string</returns>
//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 class KeyService : IKeyService { public void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream) {
/// <summary> if (csp.PublicOnly) throw new ArgumentException("CSP does not contain a private key", "csp");
/// Export a certificate to a PEM format string var parameters = csp.ExportParameters(true);
/// </summary> using (var stream = new MemoryStream()) {
/// <param name="cert">The certificate to export</param> var writer = new BinaryWriter(stream);
/// <returns>A PEM encoded string</returns> writer.Write((byte)0x30); // SEQUENCE
//public static string ExportToPEM(X509Certificate2 cert) using (var innerStream = new MemoryStream()) {
//{ var innerWriter = new BinaryWriter(innerStream);
// StringBuilder builder = new StringBuilder(); 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);
}
// builder.AppendLine("-----BEGIN CERTIFICATE-----"); var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray();
// builder.AppendLine(Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)); outputStream.WriteLine("-----BEGIN RSA PRIVATE KEY-----");
// builder.AppendLine("-----END CERTIFICATE-----"); // 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-----");
}
}
// return builder.ToString(); private void EncodeLength(BinaryWriter stream, int length) {
//} if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative");
public void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream) { if (length < 0x80) {
var parameters = csp.ExportParameters(false); // Short form
using (var stream = new MemoryStream()) { stream.Write((byte)length);
var writer = new BinaryWriter(stream); }
writer.Write((byte)0x30); // SEQUENCE else {
using (var innerStream = new MemoryStream()) { // Long form
var innerWriter = new BinaryWriter(innerStream); var temp = length;
innerWriter.Write((byte)0x30); // SEQUENCE var bytesRequired = 0;
EncodeLength(innerWriter, 13); while (temp > 0) {
innerWriter.Write((byte)0x06); // OBJECT IDENTIFIER temp >>= 8;
var rsaEncryptionOid = new byte[] { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01 }; bytesRequired++;
EncodeLength(innerWriter, rsaEncryptionOid.Length); }
innerWriter.Write(rsaEncryptionOid); stream.Write((byte)(bytesRequired | 0x80));
innerWriter.Write((byte)0x05); // NULL for (var i = bytesRequired - 1; i >= 0; i--) {
EncodeLength(innerWriter, 0); stream.Write((byte)(length >> (8 * i) & 0xff));
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) { private void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, bool forceUnsigned = true) {
if (csp.PublicOnly) throw new ArgumentException("CSP does not contain a private key", "csp"); stream.Write((byte)0x02); // INTEGER
var parameters = csp.ExportParameters(true); var prefixZeros = 0;
using (var stream = new MemoryStream()) { for (var i = 0; i < value.Length; i++) {
var writer = new BinaryWriter(stream); if (value[i] != 0) break;
writer.Write((byte)0x30); // SEQUENCE prefixZeros++;
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-----");
}
} }
if (value.Length - prefixZeros == 0) {
private void EncodeLength(BinaryWriter stream, int length) { EncodeLength(stream, 1);
if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative"); stream.Write((byte)0);
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));
}
}
} }
else {
private void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, bool forceUnsigned = true) { if (forceUnsigned && value[prefixZeros] > 0x7f) {
stream.Write((byte)0x02); // INTEGER // Add a prefix zero to force unsigned if the MSB is 1
var prefixZeros = 0; EncodeLength(stream, value.Length - prefixZeros + 1);
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); stream.Write((byte)0);
} }
else { else {
if (forceUnsigned && value[prefixZeros] > 0x7f) { EncodeLength(stream, value.Length - prefixZeros);
// Add a prefix zero to force unsigned if the MSB is 1 }
EncodeLength(stream, value.Length - prefixZeros + 1); for (var i = prefixZeros; i < value.Length; i++) {
stream.Write((byte)0); stream.Write(value[i]);
}
else {
EncodeLength(stream, value.Length - prefixZeros);
}
for (var i = prefixZeros; i < value.Length; i++) {
stream.Write(value[i]);
}
} }
} }
} }

View File

@ -1,30 +1,28 @@
using System.Diagnostics; using System.Diagnostics;
namespace MaksIT.LetsEncryptConsole.Services { namespace MaksIT.LetsEncryptConsole.Services;
public interface ITerminalService {
void Exec(string cmd);
}
public class TerminalService : ITerminalService {
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}\""
}
};
pc.Start();
pc.WaitForExit();
}
}
public interface ITerminalService {
void Exec(string cmd);
}
public class TerminalService : ITerminalService {
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}\""
}
};
pc.Start();
pc.WaitForExit();
}
} }

View File

@ -1,10 +1,5 @@
using System; 
using System.Collections.Generic; namespace MaksIT.SSHProvider;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SSHProvider { public class Configuration {
public class Configuration {
}
} }

View File

@ -5,7 +5,7 @@ using Renci.SshNet;
using Renci.SshNet.Common; using Renci.SshNet.Common;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SSHProvider { namespace MaksIT.SSHProvider {
public interface ISSHService : IDisposable { public interface ISSHService : IDisposable {
IDomainResult Upload(string workingdirectory, string fileName, byte[] bytes); IDomainResult Upload(string workingdirectory, string fileName, byte[] bytes);
@ -77,8 +77,8 @@ namespace SSHProvider {
_logger.LogInformation($"Listing directory:"); _logger.LogInformation($"Listing directory:");
foreach (var fi in listDirectory) { foreach (var file in listDirectory) {
_logger.LogInformation($" - " + fi.Name); _logger.LogInformation($" - " + file.Name);
} }
return IDomainResult.Success(); return IDomainResult.Success();

View File

@ -7,57 +7,56 @@ using Xunit;
//using PecMgr.VaultProvider; //using PecMgr.VaultProvider;
//using PecMgr.Core.Abstractions; //using PecMgr.Core.Abstractions;
namespace MaksIT.Tests.SSHProviderTests.Abstractions { namespace MaksIT.Tests.SSHProviderTests.Abstractions;
//[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] //[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public abstract class ConfigurationBase { 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() { public ConfigurationBase() {
Configuration = InitConfig(); Configuration = InitConfig();
ConfigureServices(ServiceCollection); ConfigureServices(ServiceCollection);
} }
protected abstract void ConfigureServices(IServiceCollection services); protected abstract void ConfigureServices(IServiceCollection services);
private IConfiguration InitConfig() { private IConfiguration InitConfig() {
var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var currentDirectory = Directory.GetCurrentDirectory(); var currentDirectory = Directory.GetCurrentDirectory();
var configurationBuilder = new ConfigurationBuilder() var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory()) .SetBasePath(Directory.GetCurrentDirectory())
.AddEnvironmentVariables(); .AddEnvironmentVariables();
if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) && new FileInfo(Path.Combine(currentDirectory, $"appsettings.{aspNetCoreEnvironment}.json")).Exists) if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) && new FileInfo(Path.Combine(currentDirectory, $"appsettings.{aspNetCoreEnvironment}.json")).Exists)
configurationBuilder.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true); configurationBuilder.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true);
else if (new FileInfo(Path.Combine(currentDirectory, "appsettings.json")).Exists) else if (new FileInfo(Path.Combine(currentDirectory, "appsettings.json")).Exists)
configurationBuilder.AddJsonFile("appsettings.json", true, true); configurationBuilder.AddJsonFile("appsettings.json", true, true);
else else
throw new FileNotFoundException($"Unable to find appsetting.json in {currentDirectory}"); throw new FileNotFoundException($"Unable to find appsetting.json in {currentDirectory}");
//var builtConfig = configurationBuilder.Build(); //var builtConfig = configurationBuilder.Build();
//var vaultOptions = builtConfig.GetSection("Vault"); //var vaultOptions = builtConfig.GetSection("Vault");
//configurationBuilder.AddVault(options => { //configurationBuilder.AddVault(options => {
// options.Address = vaultOptions["Address"]; // options.Address = vaultOptions["Address"];
// options.UnsealKeys = vaultOptions.GetSection("UnsealKeys").Get<List<string>>(); // options.UnsealKeys = vaultOptions.GetSection("UnsealKeys").Get<List<string>>();
// options.AuthMethod = EnumerationStringId.FromValue<AuthenticationMethod>(vaultOptions["AuthMethod"]); // options.AuthMethod = EnumerationStringId.FromValue<AuthenticationMethod>(vaultOptions["AuthMethod"]);
// options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get<AppRoleAuthMethod>(); // options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get<AppRoleAuthMethod>();
// options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get<TokenAuthMethod>(); // options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get<TokenAuthMethod>();
// options.MountPath = vaultOptions["MountPath"]; // options.MountPath = vaultOptions["MountPath"];
// options.SecretType = vaultOptions["SecretType"]; // options.SecretType = vaultOptions["SecretType"];
// options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get<Dictionary<string, string>>(); // options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get<Dictionary<string, string>>();
//}); //});
return configurationBuilder.Build(); return configurationBuilder.Build();
}
} }
} }

View File

@ -4,27 +4,26 @@ using Serilog;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using SSHProvider; using MaksIT.SSHProvider;
namespace MaksIT.Tests.SSHProviderTests.Abstractions { namespace MaksIT.Tests.SSHProviderTests.Abstractions;
public abstract class ServicesBase : ConfigurationBase { public abstract class ServicesBase : ConfigurationBase {
public ServicesBase() : base() { } public ServicesBase() : base() { }
protected override void ConfigureServices(IServiceCollection services) { protected override void ConfigureServices(IServiceCollection services) {
// configure strongly typed settings objects // configure strongly typed settings objects
var appSettingsSection = Configuration.GetSection("Configuration"); var appSettingsSection = Configuration.GetSection("Configuration");
services.Configure<Configuration>(appSettingsSection); services.Configure<Configuration>(appSettingsSection);
var appSettings = appSettingsSection.Get<Configuration>(); var appSettings = appSettingsSection.Get<Configuration>();
#region configurazione logging #region configurazione logging
services.AddLogging(configure => { services.AddLogging(configure => {
configure.AddSerilog(new LoggerConfiguration() configure.AddSerilog(new LoggerConfiguration()
//.ReadFrom.Configuration(_configuration) //.ReadFrom.Configuration(_configuration)
.CreateLogger()); .CreateLogger());
}); });
#endregion #endregion
}
} }
} }

View File

@ -3,54 +3,53 @@ using System.Security.Cryptography;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SSHProvider; using MaksIT.SSHProvider;
using MaksIT.Tests.SSHProviderTests.Abstractions; using MaksIT.Tests.SSHProviderTests.Abstractions;
namespace SSHSerivceTests { namespace MaksIT.SSHSerivceTests;
public class UnitTest1 : ServicesBase { public class UnitTest1 : ServicesBase {
public readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; public readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
[Fact] [Fact]
public void UploadFile() { public void UploadFile() {
var username = ""; var username = "";
var password = ""; var password = "";
var filePath = Path.Combine(_appPath, "randomfile.txt"); var filePath = Path.Combine(_appPath, "randomfile.txt");
CreateRandomFile(filePath, 1); CreateRandomFile(filePath, 1);
var logger = ServiceProvider.GetService<ILogger<SSHService>>(); var logger = ServiceProvider.GetService<ILogger<SSHService>>();
using var sshService = new SSHService(logger, "192.168.0.10", 22, username, password); using var sshService = new SSHService(logger, "192.168.0.10", 22, username, password);
sshService.Connect(); 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, "chown nginx:nginx /var/www/ssl -R");
sshService.RunSudoCommand(password, "chmod 777 /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, "chown nginx:nginx /var/www/ssl -R");
sshService.RunSudoCommand(password, "chmod 775 /var/www/ssl -R"); sshService.RunSudoCommand(password, "chmod 775 /var/www/ssl -R");
} }
private void CreateRandomFile(string filePath, int sizeInMb) { private void CreateRandomFile(string filePath, int sizeInMb) {
// Note: block size must be a factor of 1MB to avoid rounding errors // Note: block size must be a factor of 1MB to avoid rounding errors
const int blockSize = 1024 * 8; const int blockSize = 1024 * 8;
const int blocksPerMb = (1024 * 1024) / blockSize; const int blocksPerMb = (1024 * 1024) / blockSize;
byte[] data = new byte[blockSize]; byte[] data = new byte[blockSize];
using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) { using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) {
using (FileStream stream = File.OpenWrite(filePath)) { using (FileStream stream = File.OpenWrite(filePath)) {
for (int i = 0; i < sizeInMb * blocksPerMb; i++) { for (int i = 0; i < sizeInMb * blocksPerMb; i++) {
crypto.GetBytes(data); crypto.GetBytes(data);
stream.Write(data, 0, data.Length); stream.Write(data, 0, data.Length);
}
} }
} }
} }