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