mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): ssh and sftp
This commit is contained in:
parent
6aae71b7ac
commit
23fa0d9826
@ -5,9 +5,15 @@ VisualStudioVersion = 17.6.33815.320
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt", "LetsEncrypt\LetsEncrypt.csproj", "{7DE431E5-889C-434E-AD02-9F89D7A0ED27}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LetsEncryptConsole", "LetsEncryptConsole\LetsEncryptConsole.csproj", "{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncryptConsole", "LetsEncryptConsole\LetsEncryptConsole.csproj", "{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{27A58A5F-B52A-44F2-9639-84C6F02EA75D}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "Core\Core.csproj", "{27A58A5F-B52A-44F2-9639-84C6F02EA75D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SSHProvider", "SSHProvider\SSHProvider.csproj", "{B6556305-D728-4368-A22C-93079C236808}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3374FDB1-C95E-4103-8E14-5BBF0BDC4E9D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SSHProviderTests", "Tests\SSHSerivceTests\SSHProviderTests.csproj", "{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -27,10 +33,21 @@ Global
|
||||
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B6556305-D728-4368-A22C-93079C236808}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B6556305-D728-4368-A22C-93079C236808}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B6556305-D728-4368-A22C-93079C236808}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B6556305-D728-4368-A22C-93079C236808}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4} = {3374FDB1-C95E-4103-8E14-5BBF0BDC4E9D}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B78BD325-B2C1-456C-8EA8-42F9B89E0351}
|
||||
EndGlobalSection
|
||||
|
||||
@ -2,106 +2,104 @@
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MaksIT.LetsEncrypt.Entities.Jws
|
||||
{
|
||||
public class Jwk
|
||||
{
|
||||
/// <summary>
|
||||
/// "kty" (Key Type) Parameter
|
||||
/// <para>
|
||||
/// The "kty" (key type) parameter identifies the cryptographic algorithm
|
||||
/// family used with the key, such as "RSA" or "EC".
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[JsonPropertyName("kty")]
|
||||
public string? KeyType { get; set; }
|
||||
namespace MaksIT.LetsEncrypt.Entities.Jws {
|
||||
public class Jwk {
|
||||
/// <summary>
|
||||
/// "kty" (Key Type) Parameter
|
||||
/// <para>
|
||||
/// The "kty" (key type) parameter identifies the cryptographic algorithm
|
||||
/// family used with the key, such as "RSA" or "EC".
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[JsonPropertyName("kty")]
|
||||
public string? KeyType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// "kid" (Key ID) Parameter
|
||||
/// <para>
|
||||
/// The "kid" (key ID) parameter is used to match a specific key. This
|
||||
/// is used, for instance, to choose among a set of keys within a JWK Set
|
||||
/// during key rollover. The structure of the "kid" value is
|
||||
/// unspecified.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[JsonPropertyName("kid")]
|
||||
public string? KeyId { get; set; }
|
||||
/// <summary>
|
||||
/// "kid" (Key ID) Parameter
|
||||
/// <para>
|
||||
/// The "kid" (key ID) parameter is used to match a specific key. This
|
||||
/// is used, for instance, to choose among a set of keys within a JWK Set
|
||||
/// during key rollover. The structure of the "kid" value is
|
||||
/// unspecified.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[JsonPropertyName("kid")]
|
||||
public string? KeyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// "use" (Public Key Use) Parameter
|
||||
/// <para>
|
||||
/// The "use" (public key use) parameter identifies the intended use of
|
||||
/// the public key. The "use" parameter is employed to indicate whether
|
||||
/// a public key is used for encrypting data or verifying the signature
|
||||
/// on data.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[JsonPropertyName("use")]
|
||||
public string? Use { get; set; }
|
||||
/// <summary>
|
||||
/// "use" (Public Key Use) Parameter
|
||||
/// <para>
|
||||
/// The "use" (public key use) parameter identifies the intended use of
|
||||
/// the public key. The "use" parameter is employed to indicate whether
|
||||
/// a public key is used for encrypting data or verifying the signature
|
||||
/// on data.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[JsonPropertyName("use")]
|
||||
public string? Use { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The the modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("n")]
|
||||
public string? Modulus { get; set; }
|
||||
/// <summary>
|
||||
/// The the modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("n")]
|
||||
public string? Modulus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("e")]
|
||||
public string? Exponent { get; set; }
|
||||
/// <summary>
|
||||
/// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("e")]
|
||||
public string? Exponent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("d")]
|
||||
public string? D { get; set; }
|
||||
/// <summary>
|
||||
/// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("d")]
|
||||
public string? D { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("p")]
|
||||
public string? P { get; set; }
|
||||
/// <summary>
|
||||
/// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("p")]
|
||||
public string? P { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("q")]
|
||||
public string? Q { get; set; }
|
||||
/// <summary>
|
||||
/// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("q")]
|
||||
public string? Q { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("dp")]
|
||||
public string? DP { get; set; }
|
||||
/// <summary>
|
||||
/// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("dp")]
|
||||
public string? DP { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("dq")]
|
||||
public string? DQ { get; set; }
|
||||
/// <summary>
|
||||
/// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("dq")]
|
||||
public string? DQ { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("qi")]
|
||||
public string? InverseQ { get; set; }
|
||||
/// <summary>
|
||||
/// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("qi")]
|
||||
public string? InverseQ { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The other primes information, should they exist, null or an empty list if not specified.
|
||||
/// </summary>
|
||||
[JsonPropertyName("oth")]
|
||||
public string? OthInf { get; set; }
|
||||
/// <summary>
|
||||
/// The other primes information, should they exist, null or an empty list if not specified.
|
||||
/// </summary>
|
||||
[JsonPropertyName("oth")]
|
||||
public string? OthInf { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// "alg" (Algorithm) Parameter
|
||||
/// <para>
|
||||
/// The "alg" (algorithm) parameter identifies the algorithm intended for
|
||||
/// use with the key.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[JsonPropertyName("alg")]
|
||||
public string? Algorithm { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// "alg" (Algorithm) Parameter
|
||||
/// <para>
|
||||
/// The "alg" (algorithm) parameter identifies the algorithm intended for
|
||||
/// use with the key.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[JsonPropertyName("alg")]
|
||||
public string? Algorithm { get; set; }
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,6 @@ namespace MaksIT.LetsEncrypt.Extensions {
|
||||
public static void RegisterLetsEncrypt(this IServiceCollection services) {
|
||||
|
||||
services.AddHttpClient<ILetsEncryptService, LetsEncryptService>();
|
||||
services.AddSingleton<IJwsService, JwsService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,8 +13,7 @@ using MaksIT.Core.Extensions;
|
||||
|
||||
namespace MaksIT.LetsEncrypt.Services {
|
||||
public interface IJwsService {
|
||||
|
||||
void Init(RSA rsa, string? keyId);
|
||||
void SetKeyId(string location);
|
||||
|
||||
JwsMessage Encode(JwsHeader protectedHeader);
|
||||
|
||||
@ -22,34 +21,41 @@ namespace MaksIT.LetsEncrypt.Services {
|
||||
|
||||
string GetKeyAuthorization(string token);
|
||||
|
||||
string Base64UrlEncoded(byte[] arg);
|
||||
|
||||
void SetKeyId(string location);
|
||||
string Base64UrlEncoded(string s);
|
||||
|
||||
string Base64UrlEncoded(byte[] arg);
|
||||
}
|
||||
|
||||
|
||||
public class JwsService : IJwsService {
|
||||
|
||||
public Jwk? _jwk;
|
||||
private RSA? _rsa;
|
||||
public Jwk _jwk;
|
||||
private RSA _rsa;
|
||||
|
||||
public void Init(RSA rsa, string? keyId) {
|
||||
public JwsService(RSA rsa) {
|
||||
_rsa = rsa ?? throw new ArgumentNullException(nameof(rsa));
|
||||
|
||||
var publicParameters = rsa.ExportParameters(false);
|
||||
|
||||
var exp = publicParameters.Exponent ?? throw new ArgumentNullException(nameof(publicParameters.Exponent));
|
||||
var mod = publicParameters.Modulus ?? throw new ArgumentNullException(nameof(publicParameters.Modulus));
|
||||
|
||||
_jwk = new Jwk() {
|
||||
KeyType = "RSA",
|
||||
Exponent = Base64UrlEncoded(publicParameters.Exponent),
|
||||
Modulus = Base64UrlEncoded(publicParameters.Modulus),
|
||||
KeyId = keyId
|
||||
Exponent = Base64UrlEncoded(exp),
|
||||
Modulus = Base64UrlEncoded(mod),
|
||||
};
|
||||
}
|
||||
|
||||
public void SetKeyId(string location) {
|
||||
_jwk.KeyId = location;
|
||||
}
|
||||
|
||||
public JwsMessage Encode(JwsHeader protectedHeader) =>
|
||||
Encode<string>(null, protectedHeader);
|
||||
|
||||
public JwsMessage Encode<TPayload>(TPayload? payload, JwsHeader protectedHeader) {
|
||||
public JwsMessage Encode<T>(T? payload, JwsHeader protectedHeader) {
|
||||
|
||||
protectedHeader.Algorithm = "RS256";
|
||||
if (_jwk.KeyId != null) {
|
||||
@ -65,13 +71,10 @@ namespace MaksIT.LetsEncrypt.Services {
|
||||
};
|
||||
|
||||
if (payload != null) {
|
||||
if (payload is string) {
|
||||
string value = payload.ToString();
|
||||
message.Payload = Base64UrlEncoded(value);
|
||||
}
|
||||
else {
|
||||
if (payload is string stringPayload)
|
||||
message.Payload = Base64UrlEncoded(stringPayload);
|
||||
else
|
||||
message.Payload = Base64UrlEncoded(payload.ToJson());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -83,32 +86,27 @@ namespace MaksIT.LetsEncrypt.Services {
|
||||
return message;
|
||||
}
|
||||
|
||||
public string GetKeyAuthorization(string token) =>
|
||||
$"{token}.{GetSha256Thumbprint()}";
|
||||
|
||||
private string GetSha256Thumbprint() {
|
||||
var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}";
|
||||
|
||||
using (var sha256 = SHA256.Create()) {
|
||||
return Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(json)));
|
||||
}
|
||||
return Base64UrlEncoded(SHA256.HashData(Encoding.UTF8.GetBytes(json)));
|
||||
}
|
||||
|
||||
public string GetKeyAuthorization(string token) => $"{token}.{GetSha256Thumbprint()}";
|
||||
|
||||
|
||||
|
||||
public string Base64UrlEncoded(string s) => Base64UrlEncoded(Encoding.UTF8.GetBytes(s));
|
||||
|
||||
public string Base64UrlEncoded(string s) =>
|
||||
Base64UrlEncoded(Encoding.UTF8.GetBytes(s));
|
||||
|
||||
// https://tools.ietf.org/html/rfc4648#section-5
|
||||
public string Base64UrlEncoded(byte[] arg) {
|
||||
var s = Convert.ToBase64String(arg); // Regular base64 encoder
|
||||
s = s.Split('=')[0]; // Remove any trailing '='s
|
||||
s = s.Replace('+', '-'); // 62nd char of encoding
|
||||
s = s.Replace('/', '_'); // 63rd char of encoding
|
||||
return s;
|
||||
}
|
||||
public string Base64UrlEncoded(byte[] bytes) =>
|
||||
Convert.ToBase64String(bytes) // Regular base64 encoder
|
||||
.Split('=').First() // Remove any trailing '='s
|
||||
.Replace('+', '-') // 62nd char of encoding
|
||||
.Replace('/', '_'); // 63rd char of encoding
|
||||
|
||||
|
||||
|
||||
public void SetKeyId(string location) {
|
||||
_jwk.KeyId = location;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,9 +25,9 @@ namespace MaksIT.LetsEncrypt.Services {
|
||||
|
||||
public interface ILetsEncryptService {
|
||||
|
||||
Task ConfigureClient(string url, string[] contacts);
|
||||
Task ConfigureClient(string url);
|
||||
|
||||
Task Init(RegistrationCache? registrationCache);
|
||||
Task Init(string[] contacts, RegistrationCache? registrationCache);
|
||||
|
||||
string GetTermsOfServiceUri();
|
||||
|
||||
@ -51,10 +51,10 @@ namespace MaksIT.LetsEncrypt.Services {
|
||||
//};
|
||||
|
||||
private readonly ILogger<LetsEncryptService> _logger;
|
||||
private readonly IJwsService _jwsService;
|
||||
|
||||
private HttpClient _httpClient;
|
||||
private string[]? _contacts;
|
||||
|
||||
private IJwsService _jwsService;
|
||||
private AcmeDirectory? _directory;
|
||||
private RegistrationCache? _cache;
|
||||
|
||||
@ -65,11 +65,9 @@ namespace MaksIT.LetsEncrypt.Services {
|
||||
|
||||
public LetsEncryptService(
|
||||
ILogger<LetsEncryptService> logger,
|
||||
IJwsService jwsService,
|
||||
HttpClient httpClient
|
||||
) {
|
||||
_logger = logger;
|
||||
_jwsService = jwsService;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
@ -80,12 +78,10 @@ namespace MaksIT.LetsEncrypt.Services {
|
||||
/// <param name="url"></param>
|
||||
/// <param name="contacts"></param>
|
||||
/// <returns></returns>
|
||||
public async Task ConfigureClient(string url, string[] contacts) {
|
||||
public async Task ConfigureClient(string url) {
|
||||
|
||||
_httpClient.BaseAddress ??= new Uri(url);
|
||||
|
||||
_contacts = contacts;
|
||||
|
||||
(_directory, _) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null);
|
||||
}
|
||||
|
||||
@ -95,9 +91,9 @@ namespace MaksIT.LetsEncrypt.Services {
|
||||
/// <param name="contacts"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public async Task Init(RegistrationCache? cache) {
|
||||
public async Task Init(string? [] contacts, RegistrationCache? cache) {
|
||||
|
||||
if (_contacts == null || _contacts.Length == 0)
|
||||
if (contacts == null || contacts.Length == 0)
|
||||
throw new ArgumentNullException();
|
||||
|
||||
if (_directory == null)
|
||||
@ -105,18 +101,18 @@ namespace MaksIT.LetsEncrypt.Services {
|
||||
|
||||
var accountKey = new RSACryptoServiceProvider(4096);
|
||||
|
||||
if (cache != null) {
|
||||
if (cache != null && cache.AccountKey != null) {
|
||||
_cache = cache;
|
||||
accountKey.ImportCspBlob(_cache.AccountKey);
|
||||
accountKey.ImportCspBlob(cache.AccountKey);
|
||||
}
|
||||
|
||||
// New Account request
|
||||
_jwsService.Init(accountKey, null);
|
||||
_jwsService = new JwsService(accountKey);
|
||||
|
||||
|
||||
var letsEncryptOrder = new Account {
|
||||
TermsOfServiceAgreed = true,
|
||||
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);
|
||||
@ -183,8 +179,11 @@ namespace MaksIT.LetsEncrypt.Services {
|
||||
|
||||
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
|
||||
|
||||
if (order.Status == "ready")
|
||||
return new Dictionary<string, string>();
|
||||
|
||||
if (order.Status != "pending")
|
||||
throw new InvalidOperationException($"Created new order and expected status 'pending or ready', but got: {order.Status} \r\n {response}");
|
||||
throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}");
|
||||
|
||||
_currentOrder = order;
|
||||
|
||||
@ -311,7 +310,7 @@ namespace MaksIT.LetsEncrypt.Services {
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) {
|
||||
|
||||
_logger.LogTrace($"Invoked: {nameof(GetCertificate)}");
|
||||
_logger.LogInformation($"Invoked: {nameof(GetCertificate)}");
|
||||
|
||||
|
||||
if (_currentOrder == null)
|
||||
|
||||
@ -5,12 +5,14 @@ using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using MaksIT.Core.Extensions;
|
||||
|
||||
using MaksIT.LetsEncrypt.Services;
|
||||
using MaksIT.LetsEncrypt.Entities;
|
||||
using MaksIT.LetsEncryptConsole.Services;
|
||||
|
||||
using MaksIT.Core.Extensions;
|
||||
using System.Text.Json;
|
||||
using SSHProvider;
|
||||
using Mono.Unix.Native;
|
||||
using Serilog.Core;
|
||||
|
||||
namespace MaksIT.LetsEncryptConsole {
|
||||
|
||||
@ -26,7 +28,6 @@ namespace MaksIT.LetsEncryptConsole {
|
||||
private readonly ILogger<App> _logger;
|
||||
private readonly Configuration _appSettings;
|
||||
private readonly ILetsEncryptService _letsEncryptService;
|
||||
private readonly IJwsService _jwsService;
|
||||
private readonly IKeyService _keyService;
|
||||
private readonly ITerminalService _terminalService;
|
||||
|
||||
@ -34,87 +35,90 @@ namespace MaksIT.LetsEncryptConsole {
|
||||
ILogger<App> logger,
|
||||
IOptions<Configuration> appSettings,
|
||||
ILetsEncryptService letsEncryptService,
|
||||
IJwsService jwsService,
|
||||
IKeyService keyService,
|
||||
ITerminalService terminalService
|
||||
) {
|
||||
_logger = logger;
|
||||
_appSettings = appSettings.Value;
|
||||
_letsEncryptService = letsEncryptService;
|
||||
_jwsService = jwsService;
|
||||
_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>()) {
|
||||
try {
|
||||
_logger.LogTrace($"Let's Encrypt C# .Net Core Client, environment: {env.Name}");
|
||||
_logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}");
|
||||
|
||||
//loop all customers
|
||||
foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List<Customer>()) {
|
||||
try {
|
||||
_logger.LogTrace($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}");
|
||||
_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.LogTrace($"Managing site: {site.Name}");
|
||||
_logger.LogInformation($"Managing site: {site.Name}");
|
||||
|
||||
try {
|
||||
//define cache folder
|
||||
string cacheFolder = Path.Combine(_appPath, env.Cache, customer.Id);
|
||||
if (!Directory.Exists(cacheFolder)) {
|
||||
Directory.CreateDirectory(cacheFolder);
|
||||
//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");
|
||||
|
||||
//1. Client initialization
|
||||
_logger.LogTrace("1. Client Initialization...");
|
||||
_logger.LogInformation("1. Client Initialization...");
|
||||
|
||||
#region LetsEncrypt client configuration
|
||||
await _letsEncryptService.ConfigureClient(env.Url, customer.Contacts);
|
||||
await _letsEncryptService.ConfigureClient(env.Url);
|
||||
#endregion
|
||||
|
||||
#region LetsEncrypt local registration cache initialization
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(site.Name));
|
||||
var cacheFileName = _jwsService.Base64UrlEncoded(hash) + ".lets-encrypt.cache.json";
|
||||
var cachePath = Path.Combine(cacheFolder, cacheFileName);
|
||||
var registrationCache = (File.Exists(cacheFile)
|
||||
? File.ReadAllText(cacheFile)
|
||||
: null)
|
||||
.ToObject<RegistrationCache>();
|
||||
|
||||
var cacheFile = File.Exists(cachePath)
|
||||
? File.ReadAllText(cachePath)
|
||||
: null;
|
||||
|
||||
var registrationCache = cacheFile.ToObject<RegistrationCache>();
|
||||
await _letsEncryptService.Init(registrationCache);
|
||||
registrationCache = _letsEncryptService.GetRegistrationCache();
|
||||
await _letsEncryptService.Init(customer.Contacts, registrationCache);
|
||||
#endregion
|
||||
|
||||
#region LetsEncrypt terms of service
|
||||
_logger.LogTrace($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}");
|
||||
_logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}");
|
||||
#endregion
|
||||
|
||||
//create folder for ssl
|
||||
string ssl = Path.Combine(env.GetSSL(), site.Name);
|
||||
if (!Directory.Exists(ssl)) {
|
||||
Directory.CreateDirectory(ssl);
|
||||
}
|
||||
|
||||
// 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(ssl, $"{site.Name}.crt");
|
||||
string cert = Path.Combine(sslPath, $"{site.Name}.crt");
|
||||
//if(!File.Exists(cert))
|
||||
File.WriteAllText(cert, certRes.Certificate);
|
||||
|
||||
string key = Path.Combine(ssl, $"{site.Name}.key");
|
||||
string key = Path.Combine(sslPath, $"{site.Name}.key");
|
||||
//if(!File.Exists(key)) {
|
||||
using (StreamWriter writer = File.CreateText(key))
|
||||
_keyService.ExportPrivateKey(certRes.PrivateKey, writer);
|
||||
//}
|
||||
|
||||
_logger.LogTrace("Certificate and Key exists and valid. Restored from cache.");
|
||||
_logger.LogInformation("Certificate and Key exists and valid. Restored from cache.");
|
||||
}
|
||||
else {
|
||||
|
||||
@ -127,77 +131,89 @@ namespace MaksIT.LetsEncryptConsole {
|
||||
var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge);
|
||||
#endregion
|
||||
|
||||
switch (site.Challenge) {
|
||||
case "http-01": {
|
||||
//ensure to enable static file discovery on server in .well-known/acme-challenge
|
||||
//and listen on 80 port
|
||||
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
|
||||
|
||||
//check acme directory
|
||||
var acme = env.GetACME();
|
||||
|
||||
if (!Directory.Exists(acme)) {
|
||||
Directory.CreateDirectory(acme);
|
||||
}
|
||||
|
||||
foreach (FileInfo file in new DirectoryInfo(acme).GetFiles()) {
|
||||
if (file.LastWriteTimeUtc < DateTime.UtcNow.AddMonths(-3))
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
foreach (var result in orders) {
|
||||
Console.WriteLine($"Key: {result.Key}, Value: {result.Value}");
|
||||
string[] splitToken = result.Value.Split('.');
|
||||
|
||||
File.WriteAllText(Path.Combine(env.GetACME(), splitToken[0]), result.Value);
|
||||
case "dns-01": {
|
||||
//Manage DNS server MX record, depends from provider
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux()) {
|
||||
_terminalService.Exec($"chgrp -R nginx {env.GetACME()}");
|
||||
_terminalService.Exec($"chmod -R g+rwx {env.GetACME()}");
|
||||
default: {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "dns-01": {
|
||||
//Manage DNS server MX record, depends from provider
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#region LetsEncrypt complete challenges
|
||||
_logger.LogInformation("3. Client Complete Challange...");
|
||||
await _letsEncryptService.CompleteChallenges();
|
||||
_logger.LogInformation("Challanges comleted.");
|
||||
#endregion
|
||||
|
||||
default: {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
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 LetsEncrypt complete challenges
|
||||
_logger.LogTrace("3. Client Complete Challange...");
|
||||
await _letsEncryptService.CompleteChallenges();
|
||||
_logger.LogTrace("Challanges comleted.");
|
||||
#endregion
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// Download new certificate
|
||||
_logger.LogTrace("4. Download certificate...");
|
||||
var (cert, key) = await _letsEncryptService.GetCertificate(site.Name);
|
||||
|
||||
#region Persist cache
|
||||
registrationCache = _letsEncryptService.GetRegistrationCache();
|
||||
File.WriteAllText(cachePath, registrationCache.ToJson());
|
||||
#endregion
|
||||
|
||||
#region Save cert and key to filesystem
|
||||
certRes = new CachedCertificateResult();
|
||||
if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) {
|
||||
string certPath = Path.Combine(ssl, site.Name + ".crt");
|
||||
File.WriteAllText(certPath, certRes.Certificate);
|
||||
|
||||
string keyPath = Path.Combine(ssl, site.Name + ".key");
|
||||
using (var writer = File.CreateText(keyPath))
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogTrace("Certificate saved.");
|
||||
}
|
||||
else {
|
||||
_logger.LogError("Unable to get new cached certificate.");
|
||||
@ -212,7 +228,7 @@ namespace MaksIT.LetsEncryptConsole {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
catch (Exception ex) {
|
||||
@ -236,9 +252,6 @@ namespace MaksIT.LetsEncryptConsole {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@ -254,7 +267,7 @@ namespace MaksIT.LetsEncryptConsole {
|
||||
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
|
||||
@ -277,11 +290,34 @@ namespace MaksIT.LetsEncryptConsole {
|
||||
/// </summary>
|
||||
/// <param name="hostsToRemove"></param>
|
||||
public RegistrationCache? ResetCachedCertificate(RegistrationCache? registrationCache, IEnumerable<string> hostsToRemove) {
|
||||
if (registrationCache != null)
|
||||
if (registrationCache?.CachedCerts != null)
|
||||
foreach (var host in hostsToRemove)
|
||||
registrationCache.CachedCerts.Remove(host);
|
||||
|
||||
return registrationCache;
|
||||
}
|
||||
private void UploadFiles(
|
||||
ILogger logger,
|
||||
SSHClientSettings sshSettings,
|
||||
string workDir,
|
||||
string fileName,
|
||||
byte [] bytes,
|
||||
string owner,
|
||||
string changeMode
|
||||
) {
|
||||
|
||||
using var sshService = new SSHService(logger, sshSettings.Host, sshSettings.Port, sshSettings.Username, sshSettings.Password);
|
||||
sshService.Connect();
|
||||
|
||||
sshService.RunSudoCommand(sshSettings.Password, $"mkdir {workDir}");
|
||||
|
||||
sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R");
|
||||
sshService.RunSudoCommand(sshSettings.Password, $"chmod 777 {workDir} -R");
|
||||
|
||||
sshService.Upload($"{workDir}", fileName, bytes);
|
||||
|
||||
sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R");
|
||||
sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,9 +6,34 @@ namespace MaksIT.LetsEncryptConsole {
|
||||
public Customer[]? Customers { get; set; }
|
||||
}
|
||||
|
||||
public class OsWindows {
|
||||
public string? Path { get; set; }
|
||||
}
|
||||
|
||||
public class OsLinux {
|
||||
public string? Path { get; set; }
|
||||
|
||||
public string? Owner { get; set; }
|
||||
|
||||
public string? ChangeMode { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class OsDependant {
|
||||
public string? Windows { get; set; }
|
||||
public string? Linux { get; set; }
|
||||
public OsWindows? Windows { get; set; }
|
||||
public OsLinux? Linux { get; set; }
|
||||
}
|
||||
|
||||
public class SSHClientSettings {
|
||||
public bool Active { get; set; }
|
||||
|
||||
public string? Host { get; set; }
|
||||
|
||||
public int Port { get; set; }
|
||||
|
||||
public string? Username { get; set; }
|
||||
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@ -18,37 +43,10 @@ namespace MaksIT.LetsEncryptConsole {
|
||||
public string? Name { get; set; }
|
||||
public string? Url { get; set; }
|
||||
|
||||
private string? _cache;
|
||||
public string Cache {
|
||||
get => _cache ?? "";
|
||||
set => _cache = value;
|
||||
}
|
||||
|
||||
public OsDependant? ACME { get; set; }
|
||||
public OsDependant? SSL { get; set; }
|
||||
|
||||
|
||||
public string? GetACME() {
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
return ACME?.Windows;
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
return ACME?.Linux;
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public string? GetSSL() {
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
return SSL?.Windows;
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
return SSL?.Linux;
|
||||
|
||||
return default;
|
||||
}
|
||||
public SSHClientSettings? SSH { get; set; }
|
||||
}
|
||||
|
||||
public class Customer {
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
<ProjectReference Include="..\LetsEncrypt\LetsEncrypt.csproj" />
|
||||
<ProjectReference Include="..\SSHProvider\SSHProvider.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -119,4 +119,22 @@ Lines with labels in quotes indicate HTTP link relations.
|
||||
| | | |
|
||||
| Download | POST-as-GET order's | 200 |
|
||||
| certificate | certificate url | |
|
||||
+-------------------+--------------------------------+--------------+
|
||||
+-------------------+--------------------------------+--------------+
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|Level|Usage|
|
||||
|-----|-----|
|
||||
|Verbose|Verbose is the noisiest level, rarely (if ever) enabled for a production app.|
|
||||
|Debug|Debug is used for internal system events that are not necessarily observable from the outside, but useful when determining how something happened.|
|
||||
|Information|Information events describe things happening in the system that correspond to its responsibilities and functions. Generally these are the observable actions the system can perform.|
|
||||
|Warning|When service is degraded, endangered, or may be behaving outside of its expected parameters, Warning level events are used.|
|
||||
|Error|When functionality is unavailable or expectations broken, an Error event is used.|
|
||||
|Fatal|The most critical level, Fatal events demand immediate attention.|
|
||||
@ -8,7 +8,10 @@
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"restrictedToMinimumLevel": "Information",
|
||||
"formatter": "Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact"
|
||||
//"formatter": "Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact"
|
||||
|
||||
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -16,33 +19,71 @@
|
||||
"Configuration": {
|
||||
"Environments": [
|
||||
{
|
||||
"Active": false,
|
||||
"Active": true,
|
||||
"Name": "StagingV2",
|
||||
"Url": "https://acme-staging-v02.api.letsencrypt.org/directory",
|
||||
|
||||
"Cache": "staging_cache",
|
||||
"ACME": {
|
||||
"Linux": "/var/www/html/.well-known/acme-challenge",
|
||||
"Windows": "C:\\Windows\\Temp\\www\\html\\.well-known\\acme-challenge"
|
||||
"Linux": {
|
||||
"Path": "/var/www/html/acme-challenge",
|
||||
"Ower": "nginx:nginx",
|
||||
"ChangeMode": "775"
|
||||
},
|
||||
"Windows": {
|
||||
"Path": "C:\\inetpub\\www\\acme-challenge"
|
||||
}
|
||||
},
|
||||
"SSL": {
|
||||
"Linux": "/var/www/ssl",
|
||||
"Windows": "C:\\Windows\\Temp\\www\\ssl"
|
||||
"Linux": {
|
||||
"Path": "/var/www/ssl/staging",
|
||||
"Owner": "nginx:nginx",
|
||||
"ChangeMode": "775"
|
||||
},
|
||||
"Windows": {
|
||||
"Path": "C:\\inetpub\\www\\ssl\\staging"
|
||||
}
|
||||
},
|
||||
"SSH": {
|
||||
"Active": true,
|
||||
"Host": "192.168.0.10",
|
||||
"Port": 22,
|
||||
"Username": "",
|
||||
"Password": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"Active": true,
|
||||
"Active": false,
|
||||
"Name": "ProductionV2",
|
||||
"Url": "https://acme-v02.api.letsencrypt.org/directory",
|
||||
|
||||
"Cache": "production_cache",
|
||||
"ACME": {
|
||||
"Linux": "/var/www/html/.well-known/acme-challenge",
|
||||
"Windows": "C:\\Windows\\Temp\\www\\html\\.well-known\\acme-challenge"
|
||||
"Linux": {
|
||||
"Path": "/var/www/html/acme-challenge",
|
||||
"Owner": "nginx:nginx",
|
||||
"ChangeMode": "775"
|
||||
},
|
||||
"Windows": {
|
||||
"Path": "C:\\inetpub\\www\\acme-challenge"
|
||||
}
|
||||
},
|
||||
"SSL": {
|
||||
"Linux": "/var/www/ssl",
|
||||
"Windows": "C:\\Windows\\Temp\\www\\ssl"
|
||||
"Linux": {
|
||||
"Path": "/var/www/ssl",
|
||||
"Owner": "nginx:nginx",
|
||||
"ChangeMode": "775"
|
||||
},
|
||||
"Windows": {
|
||||
"Path": "C:\\inetpub\\www\\ssl"
|
||||
}
|
||||
},
|
||||
"SSH": {
|
||||
"Active": true,
|
||||
"Host": "192.168.0.10",
|
||||
"Port": 22,
|
||||
"Username": "",
|
||||
"Password": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -57,11 +98,17 @@
|
||||
|
||||
"Sites": [
|
||||
{
|
||||
"Active": false,
|
||||
"Active": true,
|
||||
"Name": "maks-it.com",
|
||||
"Hosts": [
|
||||
"maks-it.com",
|
||||
"www.maks-it.com"
|
||||
"www.maks-it.com",
|
||||
|
||||
"git.maks-it.com",
|
||||
"www.git.maks-it.com",
|
||||
|
||||
"hcr.maks-it.com",
|
||||
"www.hcr.maks-it.com"
|
||||
],
|
||||
"Challenge": "http-01"
|
||||
}
|
||||
@ -69,7 +116,7 @@
|
||||
},
|
||||
{
|
||||
"Id": "46337ef5-d69b-4332-b6ef-67959dfb3c2c",
|
||||
"Active": true,
|
||||
"Active": false,
|
||||
"Contacts": [
|
||||
"maksym.sadovnychyy@gmail.com",
|
||||
"anastasiia.pavlovskaia@gmail.com"
|
||||
|
||||
10
src/SSHProvider/Configuration.cs
Normal file
10
src/SSHProvider/Configuration.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SSHProvider {
|
||||
public class Configuration {
|
||||
}
|
||||
}
|
||||
16
src/SSHProvider/SSHProvider.csproj
Normal file
16
src/SSHProvider/SSHProvider.csproj
Normal file
@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DomainResult.Common" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
144
src/SSHProvider/SSHService.cs
Normal file
144
src/SSHProvider/SSHService.cs
Normal file
@ -0,0 +1,144 @@
|
||||
using DomainResults.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Renci.SshNet;
|
||||
using Renci.SshNet.Common;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SSHProvider {
|
||||
|
||||
public interface ISSHService : IDisposable {
|
||||
IDomainResult Upload(string workingdirectory, string fileName, byte[] bytes);
|
||||
|
||||
IDomainResult ListDir(string workingdirectory);
|
||||
|
||||
IDomainResult Download();
|
||||
}
|
||||
|
||||
public class SSHService : ISSHService {
|
||||
|
||||
public readonly ILogger _logger;
|
||||
|
||||
public readonly SshClient _sshClient;
|
||||
public readonly SftpClient _sftpClient;
|
||||
|
||||
|
||||
|
||||
public SSHService(
|
||||
ILogger logger,
|
||||
string host,
|
||||
int port,
|
||||
string username,
|
||||
string password
|
||||
) {
|
||||
_logger = logger;
|
||||
_sshClient = new SshClient(host, port, username, password);
|
||||
_sftpClient = new SftpClient(host, port, username, password);
|
||||
}
|
||||
|
||||
public IDomainResult Connect() {
|
||||
try {
|
||||
_sshClient.Connect();
|
||||
_sftpClient.Connect();
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex){
|
||||
_logger.LogError(ex, "SSH Service unhandled exeption");
|
||||
return IDomainResult.CriticalDependencyError();
|
||||
}
|
||||
}
|
||||
|
||||
public IDomainResult Upload(string workingdirectory, string fileName, byte[] bytes) {
|
||||
|
||||
try {
|
||||
_sftpClient.ChangeDirectory(workingdirectory);
|
||||
_logger.LogInformation($"Changed directory to {workingdirectory}");
|
||||
|
||||
using var memoryStream = new MemoryStream(bytes);
|
||||
|
||||
_logger.LogInformation($"Uploading {fileName} ({memoryStream.Length:N0} bytes)");
|
||||
|
||||
_sftpClient.BufferSize = 4 * 1024; // bypass Payload error large files
|
||||
_sftpClient.UploadFile(memoryStream, fileName);
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogError(ex, "SSH Service unhandled exeption");
|
||||
return IDomainResult.CriticalDependencyError();
|
||||
}
|
||||
}
|
||||
|
||||
public IDomainResult ListDir(string workingdirectory) {
|
||||
try {
|
||||
|
||||
var listDirectory = _sftpClient.ListDirectory(workingdirectory);
|
||||
|
||||
_logger.LogInformation($"Listing directory:");
|
||||
|
||||
foreach (var fi in listDirectory) {
|
||||
_logger.LogInformation($" - " + fi.Name);
|
||||
}
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
|
||||
catch (Exception ex) {
|
||||
_logger.LogError(ex, "SSH Service unhandled exeption");
|
||||
return IDomainResult.CriticalDependencyError();
|
||||
}
|
||||
}
|
||||
|
||||
public IDomainResult Download() {
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
public IDomainResult RunSudoCommand(string password, string command) {
|
||||
|
||||
try {
|
||||
command = $"sudo {command}";
|
||||
|
||||
|
||||
var shellStream = _sshClient.CreateShellStream("xterm", 80, 24, 800, 600, 1024, new Dictionary<TerminalModes, uint> {
|
||||
{ TerminalModes.ECHO, 53 }
|
||||
});
|
||||
|
||||
//Get logged in
|
||||
string rep = shellStream.Expect(new Regex(@"[$>]")); //expect user prompt
|
||||
//this.writeOutput(results, rep);
|
||||
_logger.LogInformation(rep);
|
||||
|
||||
//send command
|
||||
shellStream.WriteLine(command);
|
||||
rep = shellStream.Expect(new Regex(@"([$#>:])")); //expect password or user prompt
|
||||
_logger.LogInformation(rep);
|
||||
|
||||
//check to send password
|
||||
if (rep.Contains(":")) {
|
||||
//send password
|
||||
shellStream.WriteLine(password);
|
||||
rep = shellStream.Expect(new Regex(@"[$#>]")); //expect user or root prompt
|
||||
_logger.LogInformation(rep);
|
||||
}
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogError(ex, "SSH Service unhandled exeption");
|
||||
return IDomainResult.CriticalDependencyError();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_sshClient.Disconnect();
|
||||
_sshClient.Dispose();
|
||||
|
||||
_sftpClient.Disconnect();
|
||||
_sftpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs
Normal file
63
src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Xunit;
|
||||
|
||||
//using PecMgr.VaultProvider.Extensions;
|
||||
//using PecMgr.VaultProvider;
|
||||
//using PecMgr.Core.Abstractions;
|
||||
|
||||
namespace MaksIT.Tests.SSHProviderTests.Abstractions {
|
||||
//[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
|
||||
public abstract class ConfigurationBase {
|
||||
|
||||
protected IConfiguration Configuration;
|
||||
|
||||
protected ServiceCollection ServiceCollection = new ServiceCollection();
|
||||
|
||||
protected ServiceProvider ServiceProvider { get => ServiceCollection.BuildServiceProvider(); }
|
||||
|
||||
public ConfigurationBase() {
|
||||
Configuration = InitConfig();
|
||||
ConfigureServices(ServiceCollection);
|
||||
}
|
||||
|
||||
protected abstract void ConfigureServices(IServiceCollection services);
|
||||
|
||||
private IConfiguration InitConfig() {
|
||||
var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||
var currentDirectory = Directory.GetCurrentDirectory();
|
||||
|
||||
var configurationBuilder = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddEnvironmentVariables();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) && new FileInfo(Path.Combine(currentDirectory, $"appsettings.{aspNetCoreEnvironment}.json")).Exists)
|
||||
configurationBuilder.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true);
|
||||
else if (new FileInfo(Path.Combine(currentDirectory, "appsettings.json")).Exists)
|
||||
configurationBuilder.AddJsonFile("appsettings.json", true, true);
|
||||
else
|
||||
throw new FileNotFoundException($"Unable to find appsetting.json in {currentDirectory}");
|
||||
|
||||
//var builtConfig = configurationBuilder.Build();
|
||||
//var vaultOptions = builtConfig.GetSection("Vault");
|
||||
|
||||
//configurationBuilder.AddVault(options => {
|
||||
// options.Address = vaultOptions["Address"];
|
||||
|
||||
// options.UnsealKeys = vaultOptions.GetSection("UnsealKeys").Get<List<string>>();
|
||||
|
||||
// options.AuthMethod = EnumerationStringId.FromValue<AuthenticationMethod>(vaultOptions["AuthMethod"]);
|
||||
// options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get<AppRoleAuthMethod>();
|
||||
// options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get<TokenAuthMethod>();
|
||||
|
||||
// options.MountPath = vaultOptions["MountPath"];
|
||||
// options.SecretType = vaultOptions["SecretType"];
|
||||
|
||||
// options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get<Dictionary<string, string>>();
|
||||
//});
|
||||
|
||||
return configurationBuilder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs
Normal file
30
src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Serilog;
|
||||
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using SSHProvider;
|
||||
|
||||
namespace MaksIT.Tests.SSHProviderTests.Abstractions {
|
||||
public abstract class ServicesBase : ConfigurationBase {
|
||||
|
||||
public ServicesBase() : base() { }
|
||||
|
||||
protected override void ConfigureServices(IServiceCollection services) {
|
||||
// configure strongly typed settings objects
|
||||
var appSettingsSection = Configuration.GetSection("Configuration");
|
||||
services.Configure<Configuration>(appSettingsSection);
|
||||
var appSettings = appSettingsSection.Get<Configuration>();
|
||||
|
||||
#region configurazione logging
|
||||
services.AddLogging(configure => {
|
||||
configure.AddSerilog(new LoggerConfiguration()
|
||||
//.ReadFrom.Configuration(_configuration)
|
||||
.CreateLogger());
|
||||
});
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/Tests/SSHSerivceTests/SSHProviderTests.csproj
Normal file
54
src/Tests/SSHSerivceTests/SSHProviderTests.csproj
Normal file
@ -0,0 +1,54 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
|
||||
|
||||
<PackageReference Include="Serilog.Enrichers.Span" Version="3.1.0" />
|
||||
<PackageReference Include="Serilog.Expressions" Version="3.4.1" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="1.1.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SSHProvider\SSHProvider.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
58
src/Tests/SSHSerivceTests/UnitTest1.cs
Normal file
58
src/Tests/SSHSerivceTests/UnitTest1.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using SSHProvider;
|
||||
|
||||
using MaksIT.Tests.SSHProviderTests.Abstractions;
|
||||
|
||||
namespace SSHSerivceTests {
|
||||
public class UnitTest1 : ServicesBase {
|
||||
|
||||
public readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
[Fact]
|
||||
public void UploadFile() {
|
||||
|
||||
var username = "";
|
||||
var password = "";
|
||||
var filePath = Path.Combine(_appPath, "randomfile.txt");
|
||||
CreateRandomFile(filePath, 1);
|
||||
|
||||
var logger = ServiceProvider.GetService<ILogger<SSHService>>();
|
||||
|
||||
using var sshService = new SSHService(logger, "192.168.0.10", 22, username, password);
|
||||
sshService.Connect();
|
||||
|
||||
var bytes = File.ReadAllBytes(filePath);
|
||||
|
||||
logger.LogInformation($"Uploading {filePath} ({bytes.Length:N0} bytes)");
|
||||
|
||||
sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R");
|
||||
sshService.RunSudoCommand(password, "chmod 777 /var/www/ssl -R");
|
||||
|
||||
sshService.Upload("/var/www/ssl", Path.GetFileName(filePath), bytes);
|
||||
|
||||
sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R");
|
||||
sshService.RunSudoCommand(password, "chmod 775 /var/www/ssl -R");
|
||||
}
|
||||
|
||||
private void CreateRandomFile(string filePath, int sizeInMb) {
|
||||
// Note: block size must be a factor of 1MB to avoid rounding errors
|
||||
const int blockSize = 1024 * 8;
|
||||
const int blocksPerMb = (1024 * 1024) / blockSize;
|
||||
|
||||
byte[] data = new byte[blockSize];
|
||||
|
||||
using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) {
|
||||
using (FileStream stream = File.OpenWrite(filePath)) {
|
||||
for (int i = 0; i < sizeInMb * blocksPerMb; i++) {
|
||||
crypto.GetBytes(data);
|
||||
stream.Write(data, 0, data.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/Tests/SSHSerivceTests/Usings.cs
Normal file
1
src/Tests/SSHSerivceTests/Usings.cs
Normal file
@ -0,0 +1 @@
|
||||
global using Xunit;
|
||||
22
src/Tests/SSHSerivceTests/appsettings.json
Normal file
22
src/Tests/SSHSerivceTests/appsettings.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"Serilog": {
|
||||
"Using": [ "Serilog.Settings.Configuration", "Serilog.Expressions", "Serilog.Sinks.Console" ],
|
||||
"MinimumLevel": "Verbose",
|
||||
"Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ],
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"restrictedToMinimumLevel": "Verbose",
|
||||
//"formatter": "Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact"
|
||||
|
||||
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Configuration": {
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user