(feature): ssh and sftp

This commit is contained in:
Maksym Sadovnychyy 2023-07-31 21:34:12 +02:00
parent 6aae71b7ac
commit 23fa0d9826
19 changed files with 793 additions and 284 deletions

View File

@ -5,9 +5,15 @@ VisualStudioVersion = 17.6.33815.320
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt", "LetsEncrypt\LetsEncrypt.csproj", "{7DE431E5-889C-434E-AD02-9F89D7A0ED27}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt", "LetsEncrypt\LetsEncrypt.csproj", "{7DE431E5-889C-434E-AD02-9F89D7A0ED27}"
EndProject 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 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.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.ActiveCfg = Release|Any CPU
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4} = {3374FDB1-C95E-4103-8E14-5BBF0BDC4E9D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B78BD325-B2C1-456C-8EA8-42F9B89E0351} SolutionGuid = {B78BD325-B2C1-456C-8EA8-42F9B89E0351}
EndGlobalSection EndGlobalSection

View File

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

View File

@ -7,7 +7,6 @@ namespace MaksIT.LetsEncrypt.Extensions {
public static void RegisterLetsEncrypt(this IServiceCollection services) { public static void RegisterLetsEncrypt(this IServiceCollection services) {
services.AddHttpClient<ILetsEncryptService, LetsEncryptService>(); services.AddHttpClient<ILetsEncryptService, LetsEncryptService>();
services.AddSingleton<IJwsService, JwsService>();
} }
} }
} }

View File

@ -13,8 +13,7 @@ using MaksIT.Core.Extensions;
namespace MaksIT.LetsEncrypt.Services { namespace MaksIT.LetsEncrypt.Services {
public interface IJwsService { public interface IJwsService {
void SetKeyId(string location);
void Init(RSA rsa, string? keyId);
JwsMessage Encode(JwsHeader protectedHeader); JwsMessage Encode(JwsHeader protectedHeader);
@ -22,34 +21,41 @@ namespace MaksIT.LetsEncrypt.Services {
string GetKeyAuthorization(string token); 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 class JwsService : IJwsService {
public Jwk? _jwk; public Jwk _jwk;
private RSA? _rsa; private RSA _rsa;
public void Init(RSA rsa, string? keyId) { 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 mod = publicParameters.Modulus ?? throw new ArgumentNullException(nameof(publicParameters.Modulus));
_jwk = new Jwk() { _jwk = new Jwk() {
KeyType = "RSA", KeyType = "RSA",
Exponent = Base64UrlEncoded(publicParameters.Exponent), Exponent = Base64UrlEncoded(exp),
Modulus = Base64UrlEncoded(publicParameters.Modulus), Modulus = Base64UrlEncoded(mod),
KeyId = keyId
}; };
} }
public void SetKeyId(string 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<TPayload>(TPayload? 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) {
@ -65,13 +71,10 @@ namespace MaksIT.LetsEncrypt.Services {
}; };
if (payload != null) { if (payload != null) {
if (payload is string) { if (payload is string stringPayload)
string value = payload.ToString(); message.Payload = Base64UrlEncoded(stringPayload);
message.Payload = Base64UrlEncoded(value); else
}
else {
message.Payload = Base64UrlEncoded(payload.ToJson()); message.Payload = Base64UrlEncoded(payload.ToJson());
}
} }
@ -83,32 +86,27 @@ namespace MaksIT.LetsEncrypt.Services {
return message; return message;
} }
public string GetKeyAuthorization(string token) =>
$"{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)));
using (var sha256 = SHA256.Create()) {
return Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(json)));
}
} }
public string GetKeyAuthorization(string token) => $"{token}.{GetSha256Thumbprint()}";
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[] arg) { public string Base64UrlEncoded(byte[] bytes) =>
var s = Convert.ToBase64String(arg); // Regular base64 encoder Convert.ToBase64String(bytes) // Regular base64 encoder
s = s.Split('=')[0]; // Remove any trailing '='s .Split('=').First() // Remove any trailing '='s
s = s.Replace('+', '-'); // 62nd char of encoding .Replace('+', '-') // 62nd char of encoding
s = s.Replace('/', '_'); // 63rd char of encoding .Replace('/', '_'); // 63rd char of encoding
return s;
}
public void SetKeyId(string location) {
_jwk.KeyId = location;
}
} }
} }

View File

@ -25,9 +25,9 @@ namespace MaksIT.LetsEncrypt.Services {
public interface ILetsEncryptService { 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(); string GetTermsOfServiceUri();
@ -51,10 +51,10 @@ namespace MaksIT.LetsEncrypt.Services {
//}; //};
private readonly ILogger<LetsEncryptService> _logger; private readonly ILogger<LetsEncryptService> _logger;
private readonly IJwsService _jwsService;
private HttpClient _httpClient;
private string[]? _contacts;
private HttpClient _httpClient;
private IJwsService _jwsService;
private AcmeDirectory? _directory; private AcmeDirectory? _directory;
private RegistrationCache? _cache; private RegistrationCache? _cache;
@ -65,11 +65,9 @@ namespace MaksIT.LetsEncrypt.Services {
public LetsEncryptService( public LetsEncryptService(
ILogger<LetsEncryptService> logger, ILogger<LetsEncryptService> logger,
IJwsService jwsService,
HttpClient httpClient HttpClient httpClient
) { ) {
_logger = logger; _logger = logger;
_jwsService = jwsService;
_httpClient = httpClient; _httpClient = httpClient;
} }
@ -80,12 +78,10 @@ namespace MaksIT.LetsEncrypt.Services {
/// <param name="url"></param> /// <param name="url"></param>
/// <param name="contacts"></param> /// <param name="contacts"></param>
/// <returns></returns> /// <returns></returns>
public async Task ConfigureClient(string url, string[] contacts) { public async Task ConfigureClient(string url) {
_httpClient.BaseAddress ??= new Uri(url); _httpClient.BaseAddress ??= new Uri(url);
_contacts = contacts;
(_directory, _) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null); (_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="contacts"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <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(); throw new ArgumentNullException();
if (_directory == null) if (_directory == null)
@ -105,18 +101,18 @@ namespace MaksIT.LetsEncrypt.Services {
var accountKey = new RSACryptoServiceProvider(4096); var accountKey = new RSACryptoServiceProvider(4096);
if (cache != null) { if (cache != null && cache.AccountKey != null) {
_cache = cache; _cache = cache;
accountKey.ImportCspBlob(_cache.AccountKey); accountKey.ImportCspBlob(cache.AccountKey);
} }
// New Account request // New Account request
_jwsService.Init(accountKey, null); _jwsService = new JwsService(accountKey);
var letsEncryptOrder = new Account { var letsEncryptOrder = new Account {
TermsOfServiceAgreed = true, 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); 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); 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") 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; _currentOrder = order;
@ -311,7 +310,7 @@ namespace MaksIT.LetsEncrypt.Services {
/// <exception cref="InvalidOperationException"></exception> /// <exception cref="InvalidOperationException"></exception>
public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) { public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) {
_logger.LogTrace($"Invoked: {nameof(GetCertificate)}"); _logger.LogInformation($"Invoked: {nameof(GetCertificate)}");
if (_currentOrder == null) if (_currentOrder == null)

View File

@ -5,12 +5,14 @@ using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
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 MaksIT.Core.Extensions; using Mono.Unix.Native;
using System.Text.Json; using Serilog.Core;
namespace MaksIT.LetsEncryptConsole { namespace MaksIT.LetsEncryptConsole {
@ -26,7 +28,6 @@ namespace MaksIT.LetsEncryptConsole {
private readonly ILogger<App> _logger; private readonly ILogger<App> _logger;
private readonly Configuration _appSettings; private readonly Configuration _appSettings;
private readonly ILetsEncryptService _letsEncryptService; private readonly ILetsEncryptService _letsEncryptService;
private readonly IJwsService _jwsService;
private readonly IKeyService _keyService; private readonly IKeyService _keyService;
private readonly ITerminalService _terminalService; private readonly ITerminalService _terminalService;
@ -34,87 +35,90 @@ namespace MaksIT.LetsEncryptConsole {
ILogger<App> logger, ILogger<App> logger,
IOptions<Configuration> appSettings, IOptions<Configuration> appSettings,
ILetsEncryptService letsEncryptService, ILetsEncryptService letsEncryptService,
IJwsService jwsService,
IKeyService keyService, IKeyService keyService,
ITerminalService terminalService ITerminalService terminalService
) { ) {
_logger = logger; _logger = logger;
_appSettings = appSettings.Value; _appSettings = appSettings.Value;
_letsEncryptService = letsEncryptService; _letsEncryptService = letsEncryptService;
_jwsService = jwsService;
_keyService = keyService; _keyService = keyService;
_terminalService = terminalService; _terminalService = terminalService;
} }
public async Task Run(string[] args) { 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 { 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 //loop all customers
foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List<Customer>()) { foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List<Customer>()) {
try { 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 //loop each customer website
foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List<Site>()) { 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 { try {
//define cache folder //create folder for ssl
string cacheFolder = Path.Combine(_appPath, env.Cache, customer.Id); string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name);
if (!Directory.Exists(cacheFolder)) { if (!Directory.Exists(sslPath)) {
Directory.CreateDirectory(cacheFolder); Directory.CreateDirectory(sslPath);
} }
var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json");
//1. Client initialization //1. Client initialization
_logger.LogTrace("1. Client Initialization..."); _logger.LogInformation("1. Client Initialization...");
#region LetsEncrypt client configuration #region LetsEncrypt client configuration
await _letsEncryptService.ConfigureClient(env.Url, customer.Contacts); await _letsEncryptService.ConfigureClient(env.Url);
#endregion #endregion
#region LetsEncrypt local registration cache initialization #region LetsEncrypt local registration cache initialization
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(site.Name)); var registrationCache = (File.Exists(cacheFile)
var cacheFileName = _jwsService.Base64UrlEncoded(hash) + ".lets-encrypt.cache.json"; ? File.ReadAllText(cacheFile)
var cachePath = Path.Combine(cacheFolder, cacheFileName); : null)
.ToObject<RegistrationCache>();
var cacheFile = File.Exists(cachePath) await _letsEncryptService.Init(customer.Contacts, registrationCache);
? File.ReadAllText(cachePath)
: null;
var registrationCache = cacheFile.ToObject<RegistrationCache>();
await _letsEncryptService.Init(registrationCache);
registrationCache = _letsEncryptService.GetRegistrationCache();
#endregion #endregion
#region LetsEncrypt terms of service #region LetsEncrypt terms of service
_logger.LogTrace($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}"); _logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}");
#endregion #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 // get cached certificate and check if it's valid
// if valid check if cert and key exists otherwise recreate // if valid check if cert and key exists otherwise recreate
// else continue with new certificate request // else continue with new certificate request
var certRes = new CachedCertificateResult(); var certRes = new CachedCertificateResult();
if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) { 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)) //if(!File.Exists(cert))
File.WriteAllText(cert, certRes.Certificate); 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)) { //if(!File.Exists(key)) {
using (StreamWriter writer = File.CreateText(key)) using (StreamWriter writer = File.CreateText(key))
_keyService.ExportPrivateKey(certRes.PrivateKey, writer); _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 { else {
@ -127,77 +131,89 @@ namespace MaksIT.LetsEncryptConsole {
var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge); var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge);
#endregion #endregion
switch (site.Challenge) { if (orders.Count > 0) {
case "http-01": { switch (site.Challenge) {
//ensure to enable static file discovery on server in .well-known/acme-challenge case "http-01": {
//and listen on 80 port //ensure to enable static file discovery on server in .well-known/acme-challenge
//and listen on 80 port
//check acme directory foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles())
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))
file.Delete(); 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": {
foreach (var result in orders) { //Manage DNS server MX record, depends from provider
Console.WriteLine($"Key: {result.Key}, Value: {result.Value}"); throw new NotImplementedException();
string[] splitToken = result.Value.Split('.');
File.WriteAllText(Path.Combine(env.GetACME(), splitToken[0]), result.Value);
} }
if (OperatingSystem.IsLinux()) { default: {
_terminalService.Exec($"chgrp -R nginx {env.GetACME()}"); throw new NotImplementedException();
_terminalService.Exec($"chmod -R g+rwx {env.GetACME()}");
} }
}
break;
}
case "dns-01": { #region LetsEncrypt complete challenges
//Manage DNS server MX record, depends from provider _logger.LogInformation("3. Client Complete Challange...");
throw new NotImplementedException(); await _letsEncryptService.CompleteChallenges();
} _logger.LogInformation("Challanges comleted.");
#endregion
default: { await Task.Delay(1000);
throw new NotImplementedException();
} // 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 #region Save cert and key to filesystem
certRes = new CachedCertificateResult(); certRes = new CachedCertificateResult();
if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) { 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"); File.WriteAllText(Path.Combine(sslPath, site.Name + ".crt"), certRes.Certificate);
using (var writer = File.CreateText(keyPath))
using (var writer = File.CreateText(Path.Combine(sslPath, site.Name + ".key"))) {
_keyService.ExportPrivateKey(certRes.PrivateKey, writer); _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 { else {
_logger.LogError("Unable to get new cached certificate."); _logger.LogError("Unable to get new cached certificate.");
@ -236,9 +252,6 @@ namespace MaksIT.LetsEncryptConsole {
} }
} }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@ -277,11 +290,34 @@ namespace MaksIT.LetsEncryptConsole {
/// </summary> /// </summary>
/// <param name="hostsToRemove"></param> /// <param name="hostsToRemove"></param>
public RegistrationCache? ResetCachedCertificate(RegistrationCache? registrationCache, IEnumerable<string> hostsToRemove) { public RegistrationCache? ResetCachedCertificate(RegistrationCache? registrationCache, IEnumerable<string> hostsToRemove) {
if (registrationCache != null) if (registrationCache?.CachedCerts != null)
foreach (var host in hostsToRemove) foreach (var host in hostsToRemove)
registrationCache.CachedCerts.Remove(host); registrationCache.CachedCerts.Remove(host);
return registrationCache; 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");
}
} }
} }

View File

@ -6,9 +6,34 @@ namespace MaksIT.LetsEncryptConsole {
public Customer[]? Customers { get; set; } 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 class OsDependant {
public string? Windows { get; set; } public OsWindows? Windows { get; set; }
public string? Linux { 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? Name { get; set; }
public string? Url { 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? ACME { get; set; }
public OsDependant? SSL { get; set; } public OsDependant? SSL { get; set; }
public SSHClientSettings? SSH { 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 class Customer { public class Customer {

View File

@ -33,6 +33,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" /> <ProjectReference Include="..\Core\Core.csproj" />
<ProjectReference Include="..\LetsEncrypt\LetsEncrypt.csproj" /> <ProjectReference Include="..\LetsEncrypt\LetsEncrypt.csproj" />
<ProjectReference Include="..\SSHProvider\SSHProvider.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -120,3 +120,21 @@ Lines with labels in quotes indicate HTTP link relations.
| Download | POST-as-GET order's | 200 | | Download | POST-as-GET order's | 200 |
| certificate | certificate url | | | 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.|

View File

@ -8,7 +8,10 @@
"Name": "Console", "Name": "Console",
"Args": { "Args": {
"restrictedToMinimumLevel": "Information", "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": { "Configuration": {
"Environments": [ "Environments": [
{ {
"Active": false, "Active": true,
"Name": "StagingV2", "Name": "StagingV2",
"Url": "https://acme-staging-v02.api.letsencrypt.org/directory", "Url": "https://acme-staging-v02.api.letsencrypt.org/directory",
"Cache": "staging_cache", "Cache": "staging_cache",
"ACME": { "ACME": {
"Linux": "/var/www/html/.well-known/acme-challenge", "Linux": {
"Windows": "C:\\Windows\\Temp\\www\\html\\.well-known\\acme-challenge" "Path": "/var/www/html/acme-challenge",
"Ower": "nginx:nginx",
"ChangeMode": "775"
},
"Windows": {
"Path": "C:\\inetpub\\www\\acme-challenge"
}
}, },
"SSL": { "SSL": {
"Linux": "/var/www/ssl", "Linux": {
"Windows": "C:\\Windows\\Temp\\www\\ssl" "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", "Name": "ProductionV2",
"Url": "https://acme-v02.api.letsencrypt.org/directory", "Url": "https://acme-v02.api.letsencrypt.org/directory",
"Cache": "production_cache", "Cache": "production_cache",
"ACME": { "ACME": {
"Linux": "/var/www/html/.well-known/acme-challenge", "Linux": {
"Windows": "C:\\Windows\\Temp\\www\\html\\.well-known\\acme-challenge" "Path": "/var/www/html/acme-challenge",
"Owner": "nginx:nginx",
"ChangeMode": "775"
},
"Windows": {
"Path": "C:\\inetpub\\www\\acme-challenge"
}
}, },
"SSL": { "SSL": {
"Linux": "/var/www/ssl", "Linux": {
"Windows": "C:\\Windows\\Temp\\www\\ssl" "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": [ "Sites": [
{ {
"Active": false, "Active": true,
"Name": "maks-it.com", "Name": "maks-it.com",
"Hosts": [ "Hosts": [
"maks-it.com", "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" "Challenge": "http-01"
} }
@ -69,7 +116,7 @@
}, },
{ {
"Id": "46337ef5-d69b-4332-b6ef-67959dfb3c2c", "Id": "46337ef5-d69b-4332-b6ef-67959dfb3c2c",
"Active": true, "Active": false,
"Contacts": [ "Contacts": [
"maksym.sadovnychyy@gmail.com", "maksym.sadovnychyy@gmail.com",
"anastasiia.pavlovskaia@gmail.com" "anastasiia.pavlovskaia@gmail.com"

View 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 {
}
}

View 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>

View 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();
}
}
}

View 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();
}
}
}

View 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
}
}
}

View 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>

View 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);
}
}
}
}
}
}

View File

@ -0,0 +1 @@
global using Xunit;

View 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": {
}
}