From 23fa0d9826c19524cc34932b1df071a83af09d6e Mon Sep 17 00:00:00 2001 From: hailstrike Date: Mon, 31 Jul 2023 21:34:12 +0200 Subject: [PATCH] (feature): ssh and sftp --- src/LetsEncrypt.sln | 21 +- src/LetsEncrypt/Entities/Jws/Jwk.cs | 178 +++++++------- .../Extensions/ServiceCollectionExtensions.cs | 1 - src/LetsEncrypt/Services/JwsService.cs | 68 +++--- .../Services/LetsEncryptService.cs | 33 ++- src/LetsEncryptConsole/App.cs | 224 ++++++++++-------- src/LetsEncryptConsole/Configuration.cs | 58 +++-- .../LetsEncryptConsole.csproj | 1 + src/LetsEncryptConsole/README.md | 20 +- src/LetsEncryptConsole/appsettings.json | 75 ++++-- src/SSHProvider/Configuration.cs | 10 + src/SSHProvider/SSHProvider.csproj | 16 ++ src/SSHProvider/SSHService.cs | 144 +++++++++++ .../Abstractions/ConfigurationBase.cs | 63 +++++ .../Abstractions/ServiceBase.cs | 30 +++ .../SSHSerivceTests/SSHProviderTests.csproj | 54 +++++ src/Tests/SSHSerivceTests/UnitTest1.cs | 58 +++++ src/Tests/SSHSerivceTests/Usings.cs | 1 + src/Tests/SSHSerivceTests/appsettings.json | 22 ++ 19 files changed, 793 insertions(+), 284 deletions(-) create mode 100644 src/SSHProvider/Configuration.cs create mode 100644 src/SSHProvider/SSHProvider.csproj create mode 100644 src/SSHProvider/SSHService.cs create mode 100644 src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs create mode 100644 src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs create mode 100644 src/Tests/SSHSerivceTests/SSHProviderTests.csproj create mode 100644 src/Tests/SSHSerivceTests/UnitTest1.cs create mode 100644 src/Tests/SSHSerivceTests/Usings.cs create mode 100644 src/Tests/SSHSerivceTests/appsettings.json diff --git a/src/LetsEncrypt.sln b/src/LetsEncrypt.sln index 9f1c543..dcdab64 100644 --- a/src/LetsEncrypt.sln +++ b/src/LetsEncrypt.sln @@ -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 diff --git a/src/LetsEncrypt/Entities/Jws/Jwk.cs b/src/LetsEncrypt/Entities/Jws/Jwk.cs index 8e3ce9f..2b4d807 100644 --- a/src/LetsEncrypt/Entities/Jws/Jwk.cs +++ b/src/LetsEncrypt/Entities/Jws/Jwk.cs @@ -2,106 +2,104 @@ using System.Text.Json.Serialization; -namespace MaksIT.LetsEncrypt.Entities.Jws -{ - public class Jwk - { - /// - /// "kty" (Key Type) Parameter - /// - /// The "kty" (key type) parameter identifies the cryptographic algorithm - /// family used with the key, such as "RSA" or "EC". - /// - /// - [JsonPropertyName("kty")] - public string? KeyType { get; set; } +namespace MaksIT.LetsEncrypt.Entities.Jws { + public class Jwk { + /// + /// "kty" (Key Type) Parameter + /// + /// The "kty" (key type) parameter identifies the cryptographic algorithm + /// family used with the key, such as "RSA" or "EC". + /// + /// + [JsonPropertyName("kty")] + public string? KeyType { get; set; } - /// - /// "kid" (Key ID) Parameter - /// - /// The "kid" (key ID) parameter is used to match a specific key. This - /// is used, for instance, to choose among a set of keys within a JWK Set - /// during key rollover. The structure of the "kid" value is - /// unspecified. - /// - /// - [JsonPropertyName("kid")] - public string? KeyId { get; set; } + /// + /// "kid" (Key ID) Parameter + /// + /// The "kid" (key ID) parameter is used to match a specific key. This + /// is used, for instance, to choose among a set of keys within a JWK Set + /// during key rollover. The structure of the "kid" value is + /// unspecified. + /// + /// + [JsonPropertyName("kid")] + public string? KeyId { get; set; } - /// - /// "use" (Public Key Use) Parameter - /// - /// The "use" (public key use) parameter identifies the intended use of - /// the public key. The "use" parameter is employed to indicate whether - /// a public key is used for encrypting data or verifying the signature - /// on data. - /// - /// - [JsonPropertyName("use")] - public string? Use { get; set; } + /// + /// "use" (Public Key Use) Parameter + /// + /// The "use" (public key use) parameter identifies the intended use of + /// the public key. The "use" parameter is employed to indicate whether + /// a public key is used for encrypting data or verifying the signature + /// on data. + /// + /// + [JsonPropertyName("use")] + public string? Use { get; set; } - /// - /// The the modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. - /// - [JsonPropertyName("n")] - public string? Modulus { get; set; } + /// + /// The the modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. + /// + [JsonPropertyName("n")] + public string? Modulus { get; set; } - /// - /// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. - /// - [JsonPropertyName("e")] - public string? Exponent { get; set; } + /// + /// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. + /// + [JsonPropertyName("e")] + public string? Exponent { get; set; } - /// - /// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("d")] - public string? D { get; set; } + /// + /// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("d")] + public string? D { get; set; } - /// - /// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("p")] - public string? P { get; set; } + /// + /// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("p")] + public string? P { get; set; } - /// - /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("q")] - public string? Q { get; set; } + /// + /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("q")] + public string? Q { get; set; } - /// - /// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("dp")] - public string? DP { get; set; } + /// + /// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("dp")] + public string? DP { get; set; } - /// - /// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("dq")] - public string? DQ { get; set; } + /// + /// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("dq")] + public string? DQ { get; set; } - /// - /// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation. - /// - [JsonPropertyName("qi")] - public string? InverseQ { get; set; } + /// + /// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonPropertyName("qi")] + public string? InverseQ { get; set; } - /// - /// The other primes information, should they exist, null or an empty list if not specified. - /// - [JsonPropertyName("oth")] - public string? OthInf { get; set; } + /// + /// The other primes information, should they exist, null or an empty list if not specified. + /// + [JsonPropertyName("oth")] + public string? OthInf { get; set; } - /// - /// "alg" (Algorithm) Parameter - /// - /// The "alg" (algorithm) parameter identifies the algorithm intended for - /// use with the key. - /// - /// - [JsonPropertyName("alg")] - public string? Algorithm { get; set; } - } + /// + /// "alg" (Algorithm) Parameter + /// + /// The "alg" (algorithm) parameter identifies the algorithm intended for + /// use with the key. + /// + /// + [JsonPropertyName("alg")] + public string? Algorithm { get; set; } + } } \ No newline at end of file diff --git a/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs b/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs index efa02f9..9bcc721 100644 --- a/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs +++ b/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs @@ -7,7 +7,6 @@ namespace MaksIT.LetsEncrypt.Extensions { public static void RegisterLetsEncrypt(this IServiceCollection services) { services.AddHttpClient(); - services.AddSingleton(); } } } diff --git a/src/LetsEncrypt/Services/JwsService.cs b/src/LetsEncrypt/Services/JwsService.cs index b66bac6..b1d775c 100644 --- a/src/LetsEncrypt/Services/JwsService.cs +++ b/src/LetsEncrypt/Services/JwsService.cs @@ -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(null, protectedHeader); - public JwsMessage Encode(TPayload? payload, JwsHeader protectedHeader) { + public JwsMessage Encode(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; - } } } diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs index 07f6154..24c58e1 100644 --- a/src/LetsEncrypt/Services/LetsEncryptService.cs +++ b/src/LetsEncrypt/Services/LetsEncryptService.cs @@ -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 _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 logger, - IJwsService jwsService, HttpClient httpClient ) { _logger = logger; - _jwsService = jwsService; _httpClient = httpClient; } @@ -80,12 +78,10 @@ namespace MaksIT.LetsEncrypt.Services { /// /// /// - public async Task ConfigureClient(string url, string[] contacts) { + public async Task ConfigureClient(string url) { _httpClient.BaseAddress ??= new Uri(url); - _contacts = contacts; - (_directory, _) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null); } @@ -95,9 +91,9 @@ namespace MaksIT.LetsEncrypt.Services { /// /// /// - 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(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder); @@ -183,8 +179,11 @@ namespace MaksIT.LetsEncrypt.Services { var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder); + if (order.Status == "ready") + return new Dictionary(); + 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 { /// public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) { - _logger.LogTrace($"Invoked: {nameof(GetCertificate)}"); + _logger.LogInformation($"Invoked: {nameof(GetCertificate)}"); if (_currentOrder == null) diff --git a/src/LetsEncryptConsole/App.cs b/src/LetsEncryptConsole/App.cs index 8b7decd..a2a021e 100644 --- a/src/LetsEncryptConsole/App.cs +++ b/src/LetsEncryptConsole/App.cs @@ -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 _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 logger, IOptions 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()) { 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()) { 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()) { - _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(); - var cacheFile = File.Exists(cachePath) - ? File.ReadAllText(cachePath) - : null; - - var registrationCache = cacheFile.ToObject(); - 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 { } } - - - /// /// /// @@ -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 { /// /// public RegistrationCache? ResetCachedCertificate(RegistrationCache? registrationCache, IEnumerable 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"); + } } } diff --git a/src/LetsEncryptConsole/Configuration.cs b/src/LetsEncryptConsole/Configuration.cs index 59872d7..afec942 100644 --- a/src/LetsEncryptConsole/Configuration.cs +++ b/src/LetsEncryptConsole/Configuration.cs @@ -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 { diff --git a/src/LetsEncryptConsole/LetsEncryptConsole.csproj b/src/LetsEncryptConsole/LetsEncryptConsole.csproj index 15dacd2..8ce4a60 100644 --- a/src/LetsEncryptConsole/LetsEncryptConsole.csproj +++ b/src/LetsEncryptConsole/LetsEncryptConsole.csproj @@ -33,6 +33,7 @@ + diff --git a/src/LetsEncryptConsole/README.md b/src/LetsEncryptConsole/README.md index 2b63b82..6e5bd17 100644 --- a/src/LetsEncryptConsole/README.md +++ b/src/LetsEncryptConsole/README.md @@ -119,4 +119,22 @@ Lines with labels in quotes indicate HTTP link relations. | | | | | Download | POST-as-GET order's | 200 | | certificate | certificate url | | - +-------------------+--------------------------------+--------------+ \ No newline at end of file + +-------------------+--------------------------------+--------------+ + + + + + + + + + + +|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.| \ No newline at end of file diff --git a/src/LetsEncryptConsole/appsettings.json b/src/LetsEncryptConsole/appsettings.json index da791d6..21fb651 100644 --- a/src/LetsEncryptConsole/appsettings.json +++ b/src/LetsEncryptConsole/appsettings.json @@ -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} {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" diff --git a/src/SSHProvider/Configuration.cs b/src/SSHProvider/Configuration.cs new file mode 100644 index 0000000..e562e50 --- /dev/null +++ b/src/SSHProvider/Configuration.cs @@ -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 { + } +} diff --git a/src/SSHProvider/SSHProvider.csproj b/src/SSHProvider/SSHProvider.csproj new file mode 100644 index 0000000..aef8d01 --- /dev/null +++ b/src/SSHProvider/SSHProvider.csproj @@ -0,0 +1,16 @@ + + + + net7.0 + enable + enable + + + + + + + + + + diff --git a/src/SSHProvider/SSHService.cs b/src/SSHProvider/SSHService.cs new file mode 100644 index 0000000..89b61e0 --- /dev/null +++ b/src/SSHProvider/SSHService.cs @@ -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.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(); + } + } +} diff --git a/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs b/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs new file mode 100644 index 0000000..b67b6ca --- /dev/null +++ b/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs @@ -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>(); + + // options.AuthMethod = EnumerationStringId.FromValue(vaultOptions["AuthMethod"]); + // options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get(); + // options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get(); + + // options.MountPath = vaultOptions["MountPath"]; + // options.SecretType = vaultOptions["SecretType"]; + + // options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get>(); + //}); + + return configurationBuilder.Build(); + } + } +} diff --git a/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs b/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs new file mode 100644 index 0000000..7d52b27 --- /dev/null +++ b/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs @@ -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(appSettingsSection); + var appSettings = appSettingsSection.Get(); + + #region configurazione logging + services.AddLogging(configure => { + configure.AddSerilog(new LoggerConfiguration() + //.ReadFrom.Configuration(_configuration) + .CreateLogger()); + }); + #endregion + + } + } +} \ No newline at end of file diff --git a/src/Tests/SSHSerivceTests/SSHProviderTests.csproj b/src/Tests/SSHSerivceTests/SSHProviderTests.csproj new file mode 100644 index 0000000..49415f7 --- /dev/null +++ b/src/Tests/SSHSerivceTests/SSHProviderTests.csproj @@ -0,0 +1,54 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/Tests/SSHSerivceTests/UnitTest1.cs b/src/Tests/SSHSerivceTests/UnitTest1.cs new file mode 100644 index 0000000..6d87802 --- /dev/null +++ b/src/Tests/SSHSerivceTests/UnitTest1.cs @@ -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>(); + + 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); + } + } + } + } + } +} diff --git a/src/Tests/SSHSerivceTests/Usings.cs b/src/Tests/SSHSerivceTests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/src/Tests/SSHSerivceTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/Tests/SSHSerivceTests/appsettings.json b/src/Tests/SSHSerivceTests/appsettings.json new file mode 100644 index 0000000..0a219f9 --- /dev/null +++ b/src/Tests/SSHSerivceTests/appsettings.json @@ -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} {NewLine}{Exception}" + } + } + ] + }, + "Configuration": { + + } +}