mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
Merge branch 'main' of gitsrv0001:MAKS-IT/letsencryptclient
This commit is contained in:
commit
c177611798
@ -8,7 +8,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Abstractions\" />
|
<Compile Remove="Abstractions\**" />
|
||||||
|
<EmbeddedResource Remove="Abstractions\**" />
|
||||||
|
<None Remove="Abstractions\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/LetsEncrypt/Entities/LetsEncrypt/SendResult.cs
Normal file
10
src/LetsEncrypt/Entities/LetsEncrypt/SendResult.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace MaksIT.LetsEncrypt.Entities {
|
||||||
|
public class SendResult<TResult> {
|
||||||
|
|
||||||
|
public TResult? Result { get; set; }
|
||||||
|
|
||||||
|
public string? ResponseText { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,25 +1,19 @@
|
|||||||
using System;
|
using MaksIT.Core.Extensions;
|
||||||
using System.Net.Http;
|
using MaksIT.LetsEncrypt.Models.Responses;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncrypt.Exceptions {
|
namespace MaksIT.LetsEncrypt.Exceptions;
|
||||||
public class LetsEncrytException : Exception {
|
public class LetsEncrytException : Exception {
|
||||||
public LetsEncrytException(Problem problem, HttpResponseMessage response)
|
|
||||||
: base($"{problem.Type}: {problem.Detail}") {
|
|
||||||
Problem = problem;
|
|
||||||
Response = response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Problem Problem { get; }
|
public Problem? Problem { get; }
|
||||||
|
|
||||||
public HttpResponseMessage Response { get; }
|
public HttpResponseMessage Response { get; }
|
||||||
}
|
|
||||||
|
|
||||||
|
public LetsEncrytException(
|
||||||
|
Problem? problem,
|
||||||
|
HttpResponseMessage response
|
||||||
|
) : base(problem != null ? $"{problem.Type}: {problem.Detail}" : "") {
|
||||||
|
|
||||||
public class Problem {
|
Problem = problem;
|
||||||
public string Type { get; set; }
|
Response = response;
|
||||||
|
|
||||||
public string Detail { get; set; }
|
|
||||||
|
|
||||||
public string RawJson { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,8 +8,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DomainResult.Common" Version="3.1.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/LetsEncrypt/Models/Responses/Problem.cs
Normal file
15
src/LetsEncrypt/Models/Responses/Problem.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MaksIT.LetsEncrypt.Models.Responses {
|
||||||
|
public class Problem {
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public string Detail { get; set; }
|
||||||
|
|
||||||
|
public string RawJson { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,102 +11,99 @@ 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(string s);
|
||||||
|
|
||||||
string Base64UrlEncoded(byte[] arg);
|
string Base64UrlEncoded(byte[] arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class JwsService : IJwsService {
|
public class JwsService : IJwsService {
|
||||||
|
|
||||||
public Jwk _jwk;
|
public Jwk _jwk;
|
||||||
private RSA _rsa;
|
private RSA _rsa;
|
||||||
|
|
||||||
public JwsService(RSA rsa) {
|
public JwsService(RSA rsa) {
|
||||||
_rsa = rsa ?? throw new ArgumentNullException(nameof(rsa));
|
_rsa = rsa ?? throw new ArgumentNullException(nameof(rsa));
|
||||||
|
|
||||||
var publicParameters = rsa.ExportParameters(false);
|
var publicParameters = rsa.ExportParameters(false);
|
||||||
|
|
||||||
var exp = publicParameters.Exponent ?? throw new ArgumentNullException(nameof(publicParameters.Exponent));
|
var exp = publicParameters.Exponent ?? throw new ArgumentNullException(nameof(publicParameters.Exponent));
|
||||||
var mod = publicParameters.Modulus ?? throw new ArgumentNullException(nameof(publicParameters.Modulus));
|
var mod = publicParameters.Modulus ?? throw new ArgumentNullException(nameof(publicParameters.Modulus));
|
||||||
|
|
||||||
_jwk = new Jwk() {
|
_jwk = new Jwk() {
|
||||||
KeyType = "RSA",
|
KeyType = "RSA",
|
||||||
Exponent = Base64UrlEncoded(exp),
|
Exponent = Base64UrlEncoded(exp),
|
||||||
Modulus = Base64UrlEncoded(mod),
|
Modulus = Base64UrlEncoded(mod),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetKeyId(string location) {
|
public void SetKeyId(string location) {
|
||||||
_jwk.KeyId = location;
|
_jwk.KeyId = location;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JwsMessage Encode(JwsHeader protectedHeader) =>
|
public JwsMessage Encode(JwsHeader protectedHeader) =>
|
||||||
Encode<string>(null, protectedHeader);
|
Encode<string>(null, protectedHeader);
|
||||||
|
|
||||||
public JwsMessage Encode<T>(T? payload, JwsHeader protectedHeader) {
|
public JwsMessage Encode<T>(T? payload, JwsHeader protectedHeader) {
|
||||||
|
|
||||||
protectedHeader.Algorithm = "RS256";
|
protectedHeader.Algorithm = "RS256";
|
||||||
if (_jwk.KeyId != null) {
|
if (_jwk.KeyId != null) {
|
||||||
protectedHeader.KeyId = _jwk.KeyId;
|
protectedHeader.KeyId = _jwk.KeyId;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
protectedHeader.Key = _jwk;
|
protectedHeader.Key = _jwk;
|
||||||
}
|
}
|
||||||
|
|
||||||
var message = new JwsMessage {
|
var message = new JwsMessage {
|
||||||
Payload = "",
|
Payload = "",
|
||||||
Protected = Base64UrlEncoded(protectedHeader.ToJson())
|
Protected = Base64UrlEncoded(protectedHeader.ToJson())
|
||||||
};
|
};
|
||||||
|
|
||||||
if (payload != null) {
|
if (payload != null) {
|
||||||
if (payload is string stringPayload)
|
if (payload is string stringPayload)
|
||||||
message.Payload = Base64UrlEncoded(stringPayload);
|
message.Payload = Base64UrlEncoded(stringPayload);
|
||||||
else
|
else
|
||||||
message.Payload = Base64UrlEncoded(payload.ToJson());
|
message.Payload = Base64UrlEncoded(payload.ToJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
message.Signature = Base64UrlEncoded(
|
message.Signature = Base64UrlEncoded(
|
||||||
_rsa.SignData(Encoding.ASCII.GetBytes($"{message.Protected}.{message.Payload}"),
|
_rsa.SignData(Encoding.ASCII.GetBytes($"{message.Protected}.{message.Payload}"),
|
||||||
HashAlgorithmName.SHA256,
|
HashAlgorithmName.SHA256,
|
||||||
RSASignaturePadding.Pkcs1));
|
RSASignaturePadding.Pkcs1));
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetKeyAuthorization(string token) =>
|
public string GetKeyAuthorization(string token) =>
|
||||||
$"{token}.{GetSha256Thumbprint()}";
|
$"{token}.{GetSha256Thumbprint()}";
|
||||||
|
|
||||||
private string GetSha256Thumbprint() {
|
private string GetSha256Thumbprint() {
|
||||||
var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}";
|
var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}";
|
||||||
return Base64UrlEncoded(SHA256.HashData(Encoding.UTF8.GetBytes(json)));
|
return Base64UrlEncoded(SHA256.HashData(Encoding.UTF8.GetBytes(json)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public string Base64UrlEncoded(string s) =>
|
public string Base64UrlEncoded(string s) =>
|
||||||
Base64UrlEncoded(Encoding.UTF8.GetBytes(s));
|
Base64UrlEncoded(Encoding.UTF8.GetBytes(s));
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc4648#section-5
|
// https://tools.ietf.org/html/rfc4648#section-5
|
||||||
public string Base64UrlEncoded(byte[] bytes) =>
|
public string Base64UrlEncoded(byte[] bytes) =>
|
||||||
Convert.ToBase64String(bytes) // Regular base64 encoder
|
Convert.ToBase64String(bytes) // Regular base64 encoder
|
||||||
.Split('=').First() // Remove any trailing '='s
|
.Split('=').First() // Remove any trailing '='s
|
||||||
.Replace('+', '-') // 62nd char of encoding
|
.Replace('+', '-') // 62nd char of encoding
|
||||||
.Replace('/', '_'); // 63rd char of encoding
|
.Replace('/', '_'); // 63rd char of encoding
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,86 +18,100 @@ 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 DomainResults.Common;
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace MaksIT.LetsEncrypt.Services {
|
namespace MaksIT.LetsEncrypt.Services;
|
||||||
|
|
||||||
public interface ILetsEncryptService {
|
public interface ILetsEncryptService {
|
||||||
|
|
||||||
Task ConfigureClient(string url);
|
Task<IDomainResult> ConfigureClient(string url);
|
||||||
|
|
||||||
Task Init(string[] contacts, RegistrationCache? registrationCache);
|
Task<IDomainResult> Init(string[] contacts, RegistrationCache? registrationCache);
|
||||||
|
|
||||||
string GetTermsOfServiceUri();
|
RegistrationCache? GetRegistrationCache();
|
||||||
|
|
||||||
|
(string?, IDomainResult) GetTermsOfServiceUri();
|
||||||
|
|
||||||
|
|
||||||
Task<Dictionary<string, string>> NewOrder(string[] hostnames, string challengeType);
|
Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(string[] hostnames, string challengeType);
|
||||||
Task CompleteChallenges();
|
Task<IDomainResult> CompleteChallenges();
|
||||||
Task GetOrder(string[] hostnames);
|
Task<IDomainResult> GetOrder(string[] hostnames);
|
||||||
Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject);
|
Task<((X509Certificate2 Cert, RSA PrivateKey)?, IDomainResult)> GetCertificate(string subject);
|
||||||
|
}
|
||||||
|
|
||||||
RegistrationCache? GetRegistrationCache();
|
|
||||||
|
|
||||||
|
|
||||||
|
public class LetsEncryptService : ILetsEncryptService {
|
||||||
|
|
||||||
|
//private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings {
|
||||||
|
// NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
// Formatting = Formatting.Indented
|
||||||
|
//};
|
||||||
|
|
||||||
|
private readonly ILogger<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>
|
||||||
|
///
|
||||||
public class LetsEncryptService : ILetsEncryptService {
|
/// </summary>
|
||||||
|
/// <param name="url"></param>
|
||||||
//private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings {
|
/// <param name="contacts"></param>
|
||||||
// NullValueHandling = NullValueHandling.Ignore,
|
/// <returns></returns>
|
||||||
// Formatting = Formatting.Indented
|
public async Task<IDomainResult> ConfigureClient(string url) {
|
||||||
//};
|
try {
|
||||||
|
|
||||||
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);
|
_httpClient.BaseAddress ??= new Uri(url);
|
||||||
|
|
||||||
(_directory, _) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null);
|
var (directory, getAcmeDirectoryResult) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null);
|
||||||
}
|
if (!getAcmeDirectoryResult.IsSuccess)
|
||||||
|
return getAcmeDirectoryResult;
|
||||||
|
|
||||||
/// <summary>
|
_directory = directory.Result;
|
||||||
/// Account creation or Initialization from cache
|
|
||||||
/// </summary>
|
return IDomainResult.Success();
|
||||||
/// <param name="contacts"></param>
|
}
|
||||||
/// <param name="token"></param>
|
catch (Exception ex) {
|
||||||
/// <returns></returns>
|
_logger.LogError(ex, "Let's Encrypt client unhandled exception");
|
||||||
public async Task Init(string? [] contacts, RegistrationCache? cache) {
|
return IDomainResult.CriticalDependencyError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Account creation or Initialization from cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contacts"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<IDomainResult> Init(string? [] contacts, RegistrationCache? cache) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
_logger.LogInformation($"Executing {nameof(Init)}...");
|
||||||
|
|
||||||
if (contacts == null || contacts.Length == 0)
|
if (contacts == null || contacts.Length == 0)
|
||||||
throw new ArgumentNullException();
|
return IDomainResult.Failed();
|
||||||
|
|
||||||
if (_directory == null)
|
if (_directory == null)
|
||||||
throw new ArgumentNullException();
|
return IDomainResult.Failed();
|
||||||
|
|
||||||
var accountKey = new RSACryptoServiceProvider(4096);
|
var accountKey = new RSACryptoServiceProvider(4096);
|
||||||
|
|
||||||
@ -115,58 +129,82 @@ namespace MaksIT.LetsEncrypt.Services {
|
|||||||
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
|
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
var (account, response) = await SendAsync<Account>(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder);
|
var (account, postAccuntResult) = await SendAsync<Account>(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder);
|
||||||
_jwsService.SetKeyId(account.Location.ToString());
|
_jwsService.SetKeyId(account.Result.Location.ToString());
|
||||||
|
|
||||||
if (account.Status != "valid")
|
if (account.Result.Status != "valid") {
|
||||||
throw new InvalidOperationException($"Account status is not valid, was: {account.Status} \r\n {response}");
|
_logger.LogError($"Account status is not valid, was: {account.Result.Status} \r\n {account.ResponseText}");
|
||||||
|
return IDomainResult.Failed();
|
||||||
|
}
|
||||||
|
|
||||||
_cache = new RegistrationCache {
|
_cache = new RegistrationCache {
|
||||||
Location = account.Location,
|
Location = account.Result.Location,
|
||||||
AccountKey = accountKey.ExportCspBlob(true),
|
AccountKey = accountKey.ExportCspBlob(true),
|
||||||
Id = account.Id,
|
Id = account.Result.Id,
|
||||||
Key = account.Key
|
Key = account.Result.Key
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return IDomainResult.Success();
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
/// <summary>
|
_logger.LogError(ex, message);
|
||||||
///
|
return IDomainResult.CriticalDependencyError(message);
|
||||||
/// </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>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public RegistrationCache? GetRegistrationCache() =>
|
||||||
|
_cache;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Just retrive terms of service
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public (string?, IDomainResult) GetTermsOfServiceUri() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
_logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}...");
|
||||||
|
|
||||||
|
if (_directory == null) {
|
||||||
|
return IDomainResult.Failed<string?>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return IDomainResult.Success(_directory.Meta.TermsOfService);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
|
_logger.LogError(ex, message);
|
||||||
|
return IDomainResult.CriticalDependencyError<string?>(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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>?, IDomainResult)> NewOrder(string[] hostnames, string challengeType) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
_logger.LogInformation($"Executing {nameof(NewOrder)}...");
|
||||||
|
|
||||||
/// <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();
|
_challenges.Clear();
|
||||||
|
|
||||||
var letsEncryptOrder = new Order {
|
var letsEncryptOrder = new Order {
|
||||||
@ -177,27 +215,38 @@ namespace MaksIT.LetsEncrypt.Services {
|
|||||||
}).ToArray()
|
}).ToArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
|
var (order, postNewOrderResult) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
|
||||||
|
if (!postNewOrderResult.IsSuccess) {
|
||||||
|
return (null, postNewOrderResult);
|
||||||
|
}
|
||||||
|
|
||||||
if (order.Status == "ready")
|
if (order.Result.Status == "ready")
|
||||||
return new Dictionary<string, string>();
|
return IDomainResult.Success(new Dictionary<string, string>());
|
||||||
|
|
||||||
if (order.Status != "pending")
|
if (order.Result.Status != "pending") {
|
||||||
throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}");
|
_logger.LogError($"Created new order and expected status 'pending', but got: {order.Result.Status} \r\n {order.Result}");
|
||||||
|
return IDomainResult.Failed<Dictionary<string, string>?>();
|
||||||
|
}
|
||||||
|
|
||||||
_currentOrder = order;
|
_currentOrder = order.Result;
|
||||||
|
|
||||||
var results = new Dictionary<string, string>();
|
var results = new Dictionary<string, string>();
|
||||||
foreach (var item in order.Authorizations) {
|
foreach (var item in order.Result.Authorizations) {
|
||||||
var (challengeResponse, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, true, null);
|
|
||||||
|
|
||||||
if (challengeResponse.Status == "valid")
|
var (challengeResponse, postAuthorisationChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, true, null);
|
||||||
|
if (!postAuthorisationChallengeResult.IsSuccess) {
|
||||||
|
return (null, postAuthorisationChallengeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (challengeResponse.Result.Status == "valid")
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (challengeResponse.Status != "pending")
|
if (challengeResponse.Result.Status != "pending") {
|
||||||
throw new InvalidOperationException($"Expected autorization status 'pending', but got: {order.Status} \r\n {responseText}");
|
_logger.LogError($"Expected autorization status 'pending', but got: {order.Result.Status} \r\n {challengeResponse.ResponseText}");
|
||||||
|
return IDomainResult.Failed<Dictionary<string, string>?>();
|
||||||
|
}
|
||||||
|
|
||||||
var challenge = challengeResponse.Challenges.First(x => x.Type == challengeType);
|
var challenge = challengeResponse.Result.Challenges.First(x => x.Type == challengeType);
|
||||||
_challenges.Add(challenge);
|
_challenges.Add(challenge);
|
||||||
|
|
||||||
var keyToken = _jwsService.GetKeyAuthorization(challenge.Token);
|
var keyToken = _jwsService.GetKeyAuthorization(challenge.Token);
|
||||||
@ -215,7 +264,7 @@ namespace MaksIT.LetsEncrypt.Services {
|
|||||||
case "dns-01": {
|
case "dns-01": {
|
||||||
using (var sha256 = SHA256.Create()) {
|
using (var sha256 = SHA256.Create()) {
|
||||||
var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
|
var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
|
||||||
results[challengeResponse.Identifier.Value] = dnsToken;
|
results[challengeResponse.Result.Identifier.Value] = dnsToken;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -232,7 +281,7 @@ namespace MaksIT.LetsEncrypt.Services {
|
|||||||
// representation of the key authorization.
|
// representation of the key authorization.
|
||||||
|
|
||||||
case "http-01": {
|
case "http-01": {
|
||||||
results[challengeResponse.Identifier.Value] = keyToken;
|
results[challengeResponse.Result.Identifier.Value] = keyToken;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,22 +290,38 @@ namespace MaksIT.LetsEncrypt.Services {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return IDomainResult.Success(results);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
/// <summary>
|
_logger.LogError(ex, message);
|
||||||
///
|
return IDomainResult.CriticalDependencyError<Dictionary<string, string>?>(message);
|
||||||
/// </summary>
|
}
|
||||||
/// <returns></returns>
|
}
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
|
||||||
public async Task CompleteChallenges() {
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public async Task<IDomainResult> CompleteChallenges() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
|
||||||
|
|
||||||
|
if (_currentOrder?.Identifiers == null) {
|
||||||
|
return IDomainResult.Failed();
|
||||||
|
}
|
||||||
|
|
||||||
for (var index = 0; index < _challenges.Count; index++) {
|
for (var index = 0; index < _challenges.Count; index++) {
|
||||||
|
|
||||||
var challenge = _challenges[index];
|
var challenge = _challenges[index];
|
||||||
|
|
||||||
|
var start = DateTime.UtcNow;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge();
|
var authorizeChallenge = new AuthorizeChallenge();
|
||||||
|
|
||||||
switch (challenge.Type) {
|
switch (challenge.Type) {
|
||||||
case "dns-01": {
|
case "dns-01": {
|
||||||
@ -270,24 +335,46 @@ namespace MaksIT.LetsEncrypt.Services {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}");
|
var (authChallenge, postAuthChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}");
|
||||||
|
if (!postAuthChallengeResult.IsSuccess) {
|
||||||
|
return postAuthChallengeResult;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.Status == "valid")
|
if (authChallenge.Result.Status == "valid")
|
||||||
break;
|
break;
|
||||||
if (result.Status != "pending")
|
|
||||||
throw new InvalidOperationException($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {responseText}");
|
if (authChallenge.Result.Status != "pending") {
|
||||||
|
_logger.LogError($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {authChallenge.ResponseText}");
|
||||||
|
return IDomainResult.Failed();
|
||||||
|
}
|
||||||
|
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
|
|
||||||
|
if ((DateTime.UtcNow - start).Seconds > 120)
|
||||||
|
throw new TimeoutException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
return IDomainResult.Success();
|
||||||
///
|
}
|
||||||
/// </summary>
|
catch (Exception ex) {
|
||||||
/// <param name="hostnames"></param>
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
/// <returns></returns>
|
|
||||||
public async Task GetOrder(string[] hostnames) {
|
_logger.LogError(ex, message);
|
||||||
|
return IDomainResult.CriticalDependencyError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hostnames"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<IDomainResult> GetOrder(string[] hostnames) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
_logger.LogInformation($"Executing {nameof(GetOrder)}");
|
||||||
|
|
||||||
var letsEncryptOrder = new Order {
|
var letsEncryptOrder = new Order {
|
||||||
Expires = DateTime.UtcNow.AddDays(2),
|
Expires = DateTime.UtcNow.AddDays(2),
|
||||||
@ -297,24 +384,36 @@ namespace MaksIT.LetsEncrypt.Services {
|
|||||||
}).ToArray()
|
}).ToArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
|
var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
|
||||||
|
if (!postOrderResult.IsSuccess)
|
||||||
|
return postOrderResult;
|
||||||
|
|
||||||
_currentOrder = order;
|
_currentOrder = order.Result;
|
||||||
|
|
||||||
|
return IDomainResult.Success();
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
/// <summary>
|
_logger.LogError(ex, message);
|
||||||
///
|
return IDomainResult.CriticalDependencyError(message);
|
||||||
/// </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)}");
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subject"></param>
|
||||||
|
/// <returns>Cert and Private key</returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public async Task<((X509Certificate2 Cert, RSA PrivateKey)?, IDomainResult)> GetCertificate(string subject) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
_logger.LogInformation($"Executing {nameof(GetCertificate)}...");
|
||||||
|
|
||||||
if (_currentOrder == null)
|
if (_currentOrder == null) {
|
||||||
throw new ArgumentNullException();
|
return IDomainResult.Failed<(X509Certificate2 Cert, RSA PrivateKey)?>();
|
||||||
|
}
|
||||||
|
|
||||||
var key = new RSACryptoServiceProvider(4096);
|
var key = new RSACryptoServiceProvider(4096);
|
||||||
var csr = new CertificateRequest("CN=" + subject,
|
var csr = new CertificateRequest("CN=" + subject,
|
||||||
@ -340,76 +439,135 @@ namespace MaksIT.LetsEncrypt.Services {
|
|||||||
await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray());
|
await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray());
|
||||||
|
|
||||||
if (_currentOrder.Status == "ready") {
|
if (_currentOrder.Status == "ready") {
|
||||||
var (response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder);
|
var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder);
|
||||||
|
if (!postOrderResult.IsSuccess || order?.Result == null)
|
||||||
|
return (null, postOrderResult);
|
||||||
|
|
||||||
if (response.Status == "processing")
|
|
||||||
(response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Location, true, null);
|
|
||||||
|
|
||||||
if (response.Status == "valid") {
|
if (order.Result.Status == "processing") {
|
||||||
certificateUrl = response.Certificate;
|
(order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Location, true, null);
|
||||||
|
if (!postOrderResult.IsSuccess || order?.Result == null)
|
||||||
|
return (null, postOrderResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order.Result.Status == "valid") {
|
||||||
|
certificateUrl = order.Result.Certificate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((start - DateTime.UtcNow).Seconds > 120)
|
if ((DateTime.UtcNow - start).Seconds > 120)
|
||||||
throw new TimeoutException();
|
throw new TimeoutException();
|
||||||
|
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
continue;
|
|
||||||
|
|
||||||
// throw new InvalidOperationException(/*$"Invalid order status: "*/);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (pem, _) = await SendAsync<string>(HttpMethod.Post, certificateUrl, true, null);
|
var (pem, postPemResult) = await SendAsync<string>(HttpMethod.Post, certificateUrl, true, null);
|
||||||
|
if (!postPemResult.IsSuccess || pem?.Result == null)
|
||||||
|
return (null, postPemResult);
|
||||||
|
|
||||||
if (_cache == null)
|
|
||||||
throw new NullReferenceException();
|
if (_cache == null) {
|
||||||
|
_logger.LogError($"{nameof(_cache)} is null");
|
||||||
|
return IDomainResult.Failed<(X509Certificate2 Cert, RSA PrivateKey)?>();
|
||||||
|
}
|
||||||
|
|
||||||
_cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
|
_cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
|
||||||
_cache.CachedCerts[subject] = new CertificateCache {
|
_cache.CachedCerts[subject] = new CertificateCache {
|
||||||
Cert = pem,
|
Cert = pem.Result,
|
||||||
Private = key.ExportCspBlob(true)
|
Private = key.ExportCspBlob(true)
|
||||||
};
|
};
|
||||||
|
|
||||||
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem));
|
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem.Result));
|
||||||
|
|
||||||
return (cert, key);
|
return IDomainResult.Success((cert, key));
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
/// <summary>
|
_logger.LogError(ex, message);
|
||||||
///
|
return IDomainResult.CriticalDependencyError< (X509Certificate2 Cert, RSA PrivateKey)?>(message);
|
||||||
/// </summary>
|
|
||||||
/// <param name="token"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task KeyChange() {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task<IDomainResult> KeyChange() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task<IDomainResult> RevokeCertificate() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request New Nonce to be able to start POST requests
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task<(string?, IDomainResult)> NewNonce() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
_logger.LogInformation($"Executing {nameof(NewNonce)}...");
|
||||||
|
|
||||||
|
if (_directory == null)
|
||||||
|
IDomainResult.Failed();
|
||||||
|
|
||||||
|
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce));
|
||||||
|
return IDomainResult.Success(result.Headers.GetValues("Replay-Nonce").First());
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="token"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task RevokeCertificate() {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
|
_logger.LogError(ex, message);
|
||||||
|
return IDomainResult.CriticalDependencyError<string?>(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main method used to send data to LetsEncrypt
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResult"></typeparam>
|
||||||
|
/// <param name="method"></param>
|
||||||
|
/// <param name="uri"></param>
|
||||||
|
/// <param name="requestModel"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task<(SendResult<TResult>?, IDomainResult)> SendAsync<TResult>(HttpMethod method, Uri uri, bool isPostAsGet, object? requestModel) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
_logger.LogInformation($"Executing {nameof(SendAsync)}...");
|
||||||
|
|
||||||
|
//if (_jwsService == null) {
|
||||||
|
// _logger.LogError($"{nameof(_jwsService)} is null");
|
||||||
|
// return IDomainResult.Failed<SendResult<TResult>?>();
|
||||||
|
//}
|
||||||
|
|
||||||
/// <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);
|
var request = new HttpRequestMessage(method, uri);
|
||||||
|
|
||||||
_nonce = uri.OriginalString != "directory"
|
if (uri.OriginalString != "directory") {
|
||||||
? await NewNonce()
|
var (nonce, newNonceResult) = await NewNonce();
|
||||||
: default;
|
if (!newNonceResult.IsSuccess || nonce == null) {
|
||||||
|
return (null, newNonceResult);
|
||||||
|
}
|
||||||
|
|
||||||
if (message != null || isPostAsGet) {
|
_nonce = nonce;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_nonce = default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestModel != null || isPostAsGet) {
|
||||||
var jwsHeader = new JwsHeader {
|
var jwsHeader = new JwsHeader {
|
||||||
Url = uri,
|
Url = uri,
|
||||||
};
|
};
|
||||||
@ -419,7 +577,7 @@ namespace MaksIT.LetsEncrypt.Services {
|
|||||||
|
|
||||||
var encodedMessage = isPostAsGet
|
var encodedMessage = isPostAsGet
|
||||||
? _jwsService.Encode(jwsHeader)
|
? _jwsService.Encode(jwsHeader)
|
||||||
: _jwsService.Encode(message, jwsHeader);
|
: _jwsService.Encode(requestModel, jwsHeader);
|
||||||
|
|
||||||
var json = encodedMessage.ToJson();
|
var json = encodedMessage.ToJson();
|
||||||
|
|
||||||
@ -438,17 +596,15 @@ namespace MaksIT.LetsEncrypt.Services {
|
|||||||
if (method == HttpMethod.Post)
|
if (method == HttpMethod.Post)
|
||||||
_nonce = response.Headers.GetValues("Replay-Nonce").First();
|
_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();
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") {
|
if (response.Content.Headers.ContentType?.MediaType == "application/problem+json")
|
||||||
return ((TResult)(object)responseText, null);
|
throw new LetsEncrytException(responseText.ToObject<Problem>(), response);
|
||||||
|
|
||||||
|
if (response.Content.Headers.ContentType?.MediaType == "application/pem-certificate-chain" && typeof(TResult) == typeof(string)) {
|
||||||
|
return IDomainResult.Success(new SendResult<TResult> {
|
||||||
|
Result = (TResult)(object)responseText
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseContent = responseText.ToObject<TResult>();
|
var responseContent = responseText.ToObject<TResult>();
|
||||||
@ -458,20 +614,17 @@ namespace MaksIT.LetsEncrypt.Services {
|
|||||||
ihl.Location = response.Headers.Location;
|
ihl.Location = response.Headers.Location;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (responseContent, responseText);
|
return IDomainResult.Success(new SendResult<TResult> {
|
||||||
|
Result = responseContent,
|
||||||
|
ResponseText = responseText
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
/// <summary>
|
_logger.LogError(ex, message);
|
||||||
/// Request New Nonce to be able to start POST requests
|
return IDomainResult.CriticalDependencyError<SendResult<TResult>?>(message);
|
||||||
/// </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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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,257 @@ 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 ITerminalService _terminalService;
|
||||||
|
|
||||||
|
public App(
|
||||||
|
ILogger<App> logger,
|
||||||
|
IOptions<Configuration> appSettings,
|
||||||
|
ILetsEncryptService letsEncryptService,
|
||||||
|
ITerminalService terminalService
|
||||||
|
) {
|
||||||
|
_logger = logger;
|
||||||
|
_appSettings = appSettings.Value;
|
||||||
|
_letsEncryptService = letsEncryptService;
|
||||||
|
_terminalService = terminalService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class App : IApp {
|
public async Task Run(string[] args) {
|
||||||
|
|
||||||
private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
|
try {
|
||||||
|
_logger.LogInformation("Let's Encrypt client. Started...");
|
||||||
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 async Task Run(string[] args) {
|
|
||||||
|
|
||||||
_logger.LogInformation("Letsencrypt client estarted...");
|
|
||||||
|
|
||||||
foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List<LetsEncryptEnvironment>()) {
|
foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List<LetsEncryptEnvironment>()) {
|
||||||
try {
|
|
||||||
_logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}");
|
|
||||||
|
|
||||||
//loop all customers
|
_logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}");
|
||||||
foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List<Customer>()) {
|
|
||||||
try {
|
|
||||||
_logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}");
|
|
||||||
|
|
||||||
//define cache folder
|
//loop all customers
|
||||||
string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache");
|
foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List<Customer>()) {
|
||||||
if (!Directory.Exists(cachePath)) {
|
|
||||||
Directory.CreateDirectory(cachePath);
|
_logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}");
|
||||||
|
|
||||||
|
//define cache folder
|
||||||
|
string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache");
|
||||||
|
if (!Directory.Exists(cachePath)) {
|
||||||
|
Directory.CreateDirectory(cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
//check acme directory
|
||||||
|
var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme");
|
||||||
|
if (!Directory.Exists(acmePath)) {
|
||||||
|
Directory.CreateDirectory(acmePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
//loop each customer website
|
||||||
|
foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List<Site>()) {
|
||||||
|
_logger.LogInformation($"Managing site: {site.Name}");
|
||||||
|
|
||||||
|
|
||||||
|
//create folder for ssl
|
||||||
|
string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name);
|
||||||
|
if (!Directory.Exists(sslPath)) {
|
||||||
|
Directory.CreateDirectory(sslPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#region LetsEncrypt client configuration and local registration cache initialization
|
||||||
|
_logger.LogInformation("1. Client Initialization...");
|
||||||
|
|
||||||
|
await _letsEncryptService.ConfigureClient(env.Url);
|
||||||
|
|
||||||
|
var registrationCache = (File.Exists(cacheFile)
|
||||||
|
? File.ReadAllText(cacheFile)
|
||||||
|
: null)
|
||||||
|
.ToObject<RegistrationCache>();
|
||||||
|
|
||||||
|
var initResult = await _letsEncryptService.Init(customer.Contacts, registrationCache);
|
||||||
|
if (!initResult.IsSuccess) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region LetsEncrypt terms of service
|
||||||
|
_logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}");
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// get cached certificate and check if it's valid
|
||||||
|
// if valid check if cert and key exists otherwise recreate
|
||||||
|
// else continue with new certificate request
|
||||||
|
var certRes = new CachedCertificateResult();
|
||||||
|
if (registrationCache != null && registrationCache.TryGetCachedCertificate(site.Name, out certRes)) {
|
||||||
|
|
||||||
|
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate);
|
||||||
|
|
||||||
|
if (certRes.PrivateKey != null)
|
||||||
|
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem());
|
||||||
|
|
||||||
|
_logger.LogInformation("Certificate and Key exists and valid. Restored from cache.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
|
||||||
|
//create new orders
|
||||||
|
#region LetsEncrypt new order
|
||||||
|
_logger.LogInformation("2. Client New Order...");
|
||||||
|
|
||||||
|
var (orders, newOrderResult) = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge);
|
||||||
|
if (!newOrderResult.IsSuccess || orders == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
if (orders.Count > 0) {
|
||||||
|
switch (site.Challenge) {
|
||||||
|
case "http-01": {
|
||||||
|
//ensure to enable static file discovery on server in .well-known/acme-challenge
|
||||||
|
//and listen on 80 port
|
||||||
|
|
||||||
|
foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles())
|
||||||
|
file.Delete();
|
||||||
|
|
||||||
|
foreach (var result in orders) {
|
||||||
|
Console.WriteLine($"Key: {result.Key}, Value: {result.Value}");
|
||||||
|
string[] splitToken = result.Value.Split('.');
|
||||||
|
|
||||||
|
File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) {
|
||||||
|
if (env?.SSH?.Active ?? false) {
|
||||||
|
UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "dns-01": {
|
||||||
|
//Manage DNS server MX record, depends from provider
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#region LetsEncrypt complete challenges
|
||||||
|
_logger.LogInformation("3. Client Complete Challange...");
|
||||||
|
var completeChallengesResult = await _letsEncryptService.CompleteChallenges();
|
||||||
|
if (!completeChallengesResult.IsSuccess) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_logger.LogInformation("Challanges comleted.");
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
await Task.Delay(1000);
|
||||||
|
|
||||||
|
#region Download new certificate
|
||||||
|
_logger.LogInformation("4. Download certificate...");
|
||||||
|
var (certData, getCertResult) = await _letsEncryptService.GetCertificate(site.Name);
|
||||||
|
if (!getCertResult.IsSuccess || certData == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not used in this scenario
|
||||||
|
// var (cert, key) = certData.Value;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Persist cache
|
||||||
|
registrationCache = _letsEncryptService.GetRegistrationCache();
|
||||||
|
File.WriteAllText(cacheFile, registrationCache.ToJson());
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
//check acme directory
|
#region Save cert and key to filesystem
|
||||||
var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme");
|
certRes = new CachedCertificateResult();
|
||||||
if (!Directory.Exists(acmePath)) {
|
if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) {
|
||||||
Directory.CreateDirectory(acmePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
//loop each customer website
|
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate);
|
||||||
foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List<Site>()) {
|
|
||||||
_logger.LogInformation($"Managing site: {site.Name}");
|
|
||||||
|
|
||||||
try {
|
if (certRes.PrivateKey != null)
|
||||||
//create folder for ssl
|
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem());
|
||||||
string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name);
|
|
||||||
if (!Directory.Exists(sslPath)) {
|
|
||||||
Directory.CreateDirectory(sslPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json");
|
_logger.LogInformation("Certificate saved.");
|
||||||
|
|
||||||
//1. Client initialization
|
foreach (FileInfo file in new DirectoryInfo(sslPath).GetFiles()) {
|
||||||
_logger.LogInformation("1. Client Initialization...");
|
|
||||||
|
|
||||||
#region LetsEncrypt client configuration
|
if (env?.SSH?.Active ?? false) {
|
||||||
await _letsEncryptService.ConfigureClient(env.Url);
|
UploadFiles(_logger, env.SSH, $"{env.SSL.Linux.Path}/{site.Name}", file.Name, File.ReadAllBytes(file.FullName), env.SSL.Linux.Owner, env.SSL.Linux.ChangeMode);
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region LetsEncrypt local registration cache initialization
|
|
||||||
var registrationCache = (File.Exists(cacheFile)
|
|
||||||
? File.ReadAllText(cacheFile)
|
|
||||||
: null)
|
|
||||||
.ToObject<RegistrationCache>();
|
|
||||||
|
|
||||||
await _letsEncryptService.Init(customer.Contacts, registrationCache);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region LetsEncrypt terms of service
|
|
||||||
_logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}");
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
// get cached certificate and check if it's valid
|
|
||||||
// if valid check if cert and key exists otherwise recreate
|
|
||||||
// else continue with new certificate request
|
|
||||||
var certRes = new CachedCertificateResult();
|
|
||||||
if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) {
|
|
||||||
string cert = Path.Combine(sslPath, $"{site.Name}.crt");
|
|
||||||
//if(!File.Exists(cert))
|
|
||||||
File.WriteAllText(cert, certRes.Certificate);
|
|
||||||
|
|
||||||
string key = Path.Combine(sslPath, $"{site.Name}.key");
|
|
||||||
//if(!File.Exists(key)) {
|
|
||||||
using (StreamWriter writer = File.CreateText(key))
|
|
||||||
_keyService.ExportPrivateKey(certRes.PrivateKey, writer);
|
|
||||||
//}
|
|
||||||
|
|
||||||
_logger.LogInformation("Certificate and Key exists and valid. Restored from cache.");
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
throw new NotImplementedException();
|
||||||
//try to make new order
|
|
||||||
try {
|
|
||||||
//create new orders
|
|
||||||
Console.WriteLine("2. Client New Order...");
|
|
||||||
|
|
||||||
#region LetsEncrypt new order
|
|
||||||
var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
if (orders.Count > 0) {
|
|
||||||
switch (site.Challenge) {
|
|
||||||
case "http-01": {
|
|
||||||
//ensure to enable static file discovery on server in .well-known/acme-challenge
|
|
||||||
//and listen on 80 port
|
|
||||||
|
|
||||||
foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles())
|
|
||||||
file.Delete();
|
|
||||||
|
|
||||||
foreach (var result in orders) {
|
|
||||||
Console.WriteLine($"Key: {result.Key}, Value: {result.Value}");
|
|
||||||
string[] splitToken = result.Value.Split('.');
|
|
||||||
|
|
||||||
File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) {
|
|
||||||
if (env?.SSH?.Active ?? false) {
|
|
||||||
UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "dns-01": {
|
|
||||||
//Manage DNS server MX record, depends from provider
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#region LetsEncrypt complete challenges
|
|
||||||
_logger.LogInformation("3. Client Complete Challange...");
|
|
||||||
await _letsEncryptService.CompleteChallenges();
|
|
||||||
_logger.LogInformation("Challanges comleted.");
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
await Task.Delay(1000);
|
|
||||||
|
|
||||||
// Download new certificate
|
|
||||||
_logger.LogInformation("4. Download certificate...");
|
|
||||||
var (cert, key) = await _letsEncryptService.GetCertificate(site.Name);
|
|
||||||
|
|
||||||
#region Persist cache
|
|
||||||
registrationCache = _letsEncryptService.GetRegistrationCache();
|
|
||||||
File.WriteAllText(cacheFile, registrationCache.ToJson());
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Save cert and key to filesystem
|
|
||||||
certRes = new CachedCertificateResult();
|
|
||||||
if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) {
|
|
||||||
|
|
||||||
File.WriteAllText(Path.Combine(sslPath, site.Name + ".crt"), certRes.Certificate);
|
|
||||||
|
|
||||||
using (var writer = File.CreateText(Path.Combine(sslPath, site.Name + ".key"))) {
|
|
||||||
_keyService.ExportPrivateKey(certRes.PrivateKey, writer);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Certificate saved.");
|
|
||||||
|
|
||||||
foreach (FileInfo file in new DirectoryInfo(sslPath).GetFiles()) {
|
|
||||||
|
|
||||||
if (env?.SSH?.Active ?? false) {
|
|
||||||
UploadFiles(_logger, env.SSH, $"{env.SSL.Linux.Path}/{site.Name}", file.Name, File.ReadAllBytes(file.FullName), env.SSL.Linux.Owner, env.SSL.Linux.ChangeMode);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_logger.LogError("Unable to get new cached certificate.");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
_logger.LogError(ex, "");
|
|
||||||
await _letsEncryptService.GetOrder(site.Hosts);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
_logger.LogError(ex, "Customer unhandled error");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
else {
|
||||||
catch (Exception ex) {
|
_logger.LogError("Unable to get new cached certificate.");
|
||||||
_logger.LogError(ex, "Environment unhandled error");
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.Name == "ProductionV2") {
|
|
||||||
_terminalService.Exec("systemctl restart nginx");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
_logger.LogError(ex.Message.ToString());
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation($"Let's Encrypt client. Execution complete.");
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
/// <summary>
|
_logger.LogError(ex, $"Let's Encrypt client. Unhandled exception.");
|
||||||
///
|
|
||||||
/// </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");
|
private void UploadFiles(
|
||||||
sshService.RunSudoCommand(sshSettings.Password, $"chmod 777 {workDir} -R");
|
ILogger logger,
|
||||||
|
SSHClientSettings sshSettings,
|
||||||
|
string workDir,
|
||||||
|
string fileName,
|
||||||
|
byte[] bytes,
|
||||||
|
string owner,
|
||||||
|
string changeMode
|
||||||
|
) {
|
||||||
|
using var sshService = new SSHService(logger, sshSettings.Host, sshSettings.Port, sshSettings.Username, sshSettings.Password);
|
||||||
|
sshService.Connect();
|
||||||
|
|
||||||
sshService.Upload($"{workDir}", fileName, bytes);
|
sshService.RunSudoCommand(sshSettings.Password, $"mkdir {workDir}");
|
||||||
|
|
||||||
sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R");
|
sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R");
|
||||||
sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R");
|
sshService.RunSudoCommand(sshSettings.Password, $"chmod 777 {workDir} -R");
|
||||||
}
|
|
||||||
|
sshService.Upload($"{workDir}", fileName, bytes);
|
||||||
|
|
||||||
|
sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R");
|
||||||
|
sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R");
|
||||||
|
|
||||||
|
//sshService.RunSudoCommand(sshSettings.Password, $"systemctl restart nginx");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@ -6,65 +6,64 @@ 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<ITerminalService, TerminalService>();
|
||||||
|
#endregion
|
||||||
|
|
||||||
services.AddSingleton<IKeyService, KeyService>();
|
// add app
|
||||||
services.AddSingleton<ITerminalService, TerminalService>();
|
services.AddSingleton<App>();
|
||||||
#endregion
|
}
|
||||||
|
|
||||||
// add app
|
private static IConfiguration InitConfig() {
|
||||||
services.AddSingleton<App>();
|
var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||||
}
|
|
||||||
|
|
||||||
private static IConfiguration InitConfig() {
|
var configuration = new ConfigurationBuilder()
|
||||||
var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
|
.AddEnvironmentVariables();
|
||||||
|
|
||||||
var configuration = new ConfigurationBuilder()
|
if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment)
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
&& new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), $"appsettings.{aspNetCoreEnvironment}.json")).Exists
|
||||||
.AddEnvironmentVariables();
|
)
|
||||||
|
configuration.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true);
|
||||||
|
else
|
||||||
|
configuration.AddJsonFile($"appsettings.json", true, true);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment)
|
return configuration.Build();
|
||||||
&& new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), $"appsettings.{aspNetCoreEnvironment}.json")).Exists
|
|
||||||
)
|
|
||||||
configuration.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true);
|
|
||||||
else
|
|
||||||
configuration.AddJsonFile($"appsettings.json", true, true);
|
|
||||||
|
|
||||||
return configuration.Build();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,151 +0,0 @@
|
|||||||
using System.Security.Cryptography;
|
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptConsole.Services {
|
|
||||||
|
|
||||||
public interface IKeyService {
|
|
||||||
void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream);
|
|
||||||
void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class KeyService : IKeyService {
|
|
||||||
/// <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 void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream) {
|
|
||||||
if (csp.PublicOnly) throw new ArgumentException("CSP does not contain a private key", "csp");
|
|
||||||
var parameters = csp.ExportParameters(true);
|
|
||||||
using (var stream = new MemoryStream()) {
|
|
||||||
var writer = new BinaryWriter(stream);
|
|
||||||
writer.Write((byte)0x30); // SEQUENCE
|
|
||||||
using (var innerStream = new MemoryStream()) {
|
|
||||||
var innerWriter = new BinaryWriter(innerStream);
|
|
||||||
EncodeIntegerBigEndian(innerWriter, new byte[] { 0x00 }); // Version
|
|
||||||
EncodeIntegerBigEndian(innerWriter, parameters.Modulus);
|
|
||||||
EncodeIntegerBigEndian(innerWriter, parameters.Exponent);
|
|
||||||
EncodeIntegerBigEndian(innerWriter, parameters.D);
|
|
||||||
EncodeIntegerBigEndian(innerWriter, parameters.P);
|
|
||||||
EncodeIntegerBigEndian(innerWriter, parameters.Q);
|
|
||||||
EncodeIntegerBigEndian(innerWriter, parameters.DP);
|
|
||||||
EncodeIntegerBigEndian(innerWriter, parameters.DQ);
|
|
||||||
EncodeIntegerBigEndian(innerWriter, parameters.InverseQ);
|
|
||||||
var length = (int)innerStream.Length;
|
|
||||||
EncodeLength(writer, length);
|
|
||||||
writer.Write(innerStream.GetBuffer(), 0, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray();
|
|
||||||
outputStream.WriteLine("-----BEGIN RSA PRIVATE KEY-----");
|
|
||||||
// Output as Base64 with lines chopped at 64 characters
|
|
||||||
for (var i = 0; i < base64.Length; i += 64) {
|
|
||||||
outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i));
|
|
||||||
}
|
|
||||||
outputStream.WriteLine("-----END RSA PRIVATE KEY-----");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EncodeLength(BinaryWriter stream, int length) {
|
|
||||||
if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative");
|
|
||||||
if (length < 0x80) {
|
|
||||||
// Short form
|
|
||||||
stream.Write((byte)length);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Long form
|
|
||||||
var temp = length;
|
|
||||||
var bytesRequired = 0;
|
|
||||||
while (temp > 0) {
|
|
||||||
temp >>= 8;
|
|
||||||
bytesRequired++;
|
|
||||||
}
|
|
||||||
stream.Write((byte)(bytesRequired | 0x80));
|
|
||||||
for (var i = bytesRequired - 1; i >= 0; i--) {
|
|
||||||
stream.Write((byte)(length >> (8 * i) & 0xff));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, bool forceUnsigned = true) {
|
|
||||||
stream.Write((byte)0x02); // INTEGER
|
|
||||||
var prefixZeros = 0;
|
|
||||||
for (var i = 0; i < value.Length; i++) {
|
|
||||||
if (value[i] != 0) break;
|
|
||||||
prefixZeros++;
|
|
||||||
}
|
|
||||||
if (value.Length - prefixZeros == 0) {
|
|
||||||
EncodeLength(stream, 1);
|
|
||||||
stream.Write((byte)0);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (forceUnsigned && value[prefixZeros] > 0x7f) {
|
|
||||||
// Add a prefix zero to force unsigned if the MSB is 1
|
|
||||||
EncodeLength(stream, value.Length - prefixZeros + 1);
|
|
||||||
stream.Write((byte)0);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
EncodeLength(stream, value.Length - prefixZeros);
|
|
||||||
}
|
|
||||||
for (var i = prefixZeros; i < value.Length; i++) {
|
|
||||||
stream.Write(value[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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 {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DomainResult.Common" Version="3.1.0" />
|
<PackageReference Include="DomainResult.Common" Version="3.1.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
||||||
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
using DomainResults.Common;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
using DomainResults.Common;
|
||||||
|
|
||||||
using Renci.SshNet;
|
using Renci.SshNet;
|
||||||
using Renci.SshNet.Common;
|
using Renci.SshNet.Common;
|
||||||
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);
|
||||||
@ -22,8 +25,6 @@ namespace SSHProvider {
|
|||||||
public readonly SshClient _sshClient;
|
public readonly SshClient _sshClient;
|
||||||
public readonly SftpClient _sftpClient;
|
public readonly SftpClient _sftpClient;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public SSHService(
|
public SSHService(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
string host,
|
string host,
|
||||||
@ -31,11 +32,40 @@ namespace SSHProvider {
|
|||||||
string username,
|
string username,
|
||||||
string password
|
string password
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
if(string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||||
|
throw new ArgumentNullException($"{nameof(username)} or {nameof(password)} is null, empty or white space");
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_sshClient = new SshClient(host, port, username, password);
|
_sshClient = new SshClient(host, port, username, password);
|
||||||
_sftpClient = new SftpClient(host, port, username, password);
|
_sftpClient = new SftpClient(host, port, username, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public SSHService(
|
||||||
|
ILogger logger,
|
||||||
|
string host,
|
||||||
|
int port,
|
||||||
|
string username,
|
||||||
|
string [] privateKeys
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(username) || privateKeys.Any(x => string.IsNullOrWhiteSpace(x)))
|
||||||
|
throw new ArgumentNullException($"{nameof(username)} or {nameof(privateKeys)} contains key which is null, empty or white space");
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
var privateKeyFiles = new List<PrivateKeyFile>();
|
||||||
|
foreach (var privateKey in privateKeys) {
|
||||||
|
using (var ms = new MemoryStream(Encoding.ASCII.GetBytes(privateKey))) {
|
||||||
|
privateKeyFiles.Add(new PrivateKeyFile(ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sshClient = new SshClient(host, port, username, privateKeyFiles.ToArray());
|
||||||
|
_sftpClient = new SftpClient(host, port, username, privateKeyFiles.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
public IDomainResult Connect() {
|
public IDomainResult Connect() {
|
||||||
try {
|
try {
|
||||||
_sshClient.Connect();
|
_sshClient.Connect();
|
||||||
@ -77,8 +107,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();
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -10,13 +10,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
|
||||||
<PackageReference Include="xunit" Version="2.4.2" />
|
<PackageReference Include="xunit" Version="2.5.0" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user