diff --git a/LetsEncrypt/ACMEv2/Directory.cs b/LetsEncrypt/ACMEv2/AcmeDirectory.cs similarity index 80% rename from LetsEncrypt/ACMEv2/Directory.cs rename to LetsEncrypt/ACMEv2/AcmeDirectory.cs index c6863bc..b060fdd 100644 --- a/LetsEncrypt/ACMEv2/Directory.cs +++ b/LetsEncrypt/ACMEv2/AcmeDirectory.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace ACMEv2 { - public class Directory + public class AcmeDirectory { //New nonce [JsonProperty("newNonce")] @@ -32,6 +32,12 @@ namespace ACMEv2 //Metadata object [JsonProperty("meta")] - public DirectoryMeta Meta { get; set; } + public AcmeDirectoryMeta Meta { get; set; } + } + + public class AcmeDirectoryMeta + { + [JsonProperty("termsOfService")] + public string TermsOfService { get; set; } } } diff --git a/LetsEncrypt/ACMEv2/DirectoryMeta.cs b/LetsEncrypt/ACMEv2/DirectoryMeta.cs deleted file mode 100644 index cf9c308..0000000 --- a/LetsEncrypt/ACMEv2/DirectoryMeta.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace ACMEv2 -{ - public class DirectoryMeta - { - [JsonProperty("termsOfService")] - public string TermsOfService { get; set; } - } - - -} diff --git a/LetsEncrypt/ACMEv2/LetsEncryptClient.cs b/LetsEncrypt/ACMEv2/LetsEncryptClient.cs index 93f898f..5d10313 100644 --- a/LetsEncrypt/ACMEv2/LetsEncryptClient.cs +++ b/LetsEncrypt/ACMEv2/LetsEncryptClient.cs @@ -76,7 +76,7 @@ namespace ACMEv2 private RSACryptoServiceProvider _accountKey; private RegistrationCache _cache; private HttpClient _client; - private Directory _directory; + private AcmeDirectory _directory; private List _challenges = new List(); private Order _currentOrder; @@ -84,6 +84,7 @@ namespace ACMEv2 /// Let's encrypt client object /// /// + /// public LetsEncryptClient(string url, string home) { _url = url ?? throw new ArgumentNullException(nameof(url)); @@ -108,7 +109,7 @@ namespace ACMEv2 _client = GetCachedClient(_url); // 1 - Get directory - (_directory, _) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), null, token); + (_directory, _) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), null, token); if (File.Exists(_path)) @@ -502,7 +503,7 @@ namespace ACMEv2 /// /// /// - public bool TryGetCachedCertificate(List hosts, out CachedCertificateResult value) + public bool TryGetCachedCertificate(string [] hosts, out CachedCertificateResult value) { value = null; if (_cache.CachedCerts.TryGetValue(hosts[0], out var cache) == false) diff --git a/LetsEncrypt/LetsEncrypt.csproj b/LetsEncrypt/LetsEncrypt.csproj index faef3b1..493f066 100644 --- a/LetsEncrypt/LetsEncrypt.csproj +++ b/LetsEncrypt/LetsEncrypt.csproj @@ -9,4 +9,10 @@ + + + PreserveNewest + + + diff --git a/LetsEncrypt/Library.cs b/LetsEncrypt/Library.cs index 4c394c7..76e6a81 100644 --- a/LetsEncrypt/Library.cs +++ b/LetsEncrypt/Library.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -172,6 +173,33 @@ namespace LetsEncrypt } } } + + + + + public static string RestoreCon(string cmd) + { + var escapedArgs = cmd.Replace("\"", "\\\""); + + var process = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = "restorecon", + Arguments = $"-v \"{escapedArgs}\"", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.Start(); + string result = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + return result; + } + + + } diff --git a/LetsEncrypt/Program.cs b/LetsEncrypt/Program.cs index 063382d..61979db 100644 --- a/LetsEncrypt/Program.cs +++ b/LetsEncrypt/Program.cs @@ -1,132 +1,179 @@ using System; using System.Collections.Generic; using System.IO; - -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; - using System.Threading.Tasks; - - - using ACMEv2; - -using FS = System.IO; - - - namespace LetsEncrypt { class Program { + private static readonly string AppPath = AppDomain.CurrentDomain.BaseDirectory; + + + static void Main(string[] args) { - // save to http:///.well-known/acme-challenge/ - var tokensPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ".well-known/acme-challenge"); - if (!FS.Directory.Exists(tokensPath)) - FS.Directory.CreateDirectory(tokensPath); - - foreach (FileInfo file in new DirectoryInfo(tokensPath).GetFiles()) - file.Delete(); - - - var certsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "certs"); - if (!FS.Directory.Exists(certsPath)) - FS.Directory.CreateDirectory(certsPath); - - - List contacts = new List(); - contacts.Add("maksym.sadovnychyy@gmail.com"); - - List hosts = new List(); - hosts.Add("maks-it.com"); - hosts.Add("www.maks-it.com"); - - Console.WriteLine("Let's Encrypt C# .Net Core Client"); - try { - LetsEncryptClient client = new LetsEncryptClient(LetsEncryptClient.ProductionV2, AppDomain.CurrentDomain.BaseDirectory); - Console.WriteLine("1. Client Initialization..."); + Console.WriteLine("Let's Encrypt C# .Net Core Client"); - // 1 - client.Init(contacts.ToArray()).Wait(); - Console.WriteLine(string.Format("Terms of service: {0}",client.GetTermsOfServiceUri())); + Settings settings = (new SettingsProvider(null)).settings; + + //loop all customers + foreach(Customer customer in settings.customers) { + try { + Console.WriteLine(string.Format("Managing customer: {0} - {1} {2}", customer.id, customer.name, customer.lastname)); + + //loop each customer website + foreach(Site site in customer.sites) { + Console.WriteLine(string.Format("Managing site: {0}", site.name)); + + try { + //define cache folder + string cache = Path.Combine(AppPath, "cache", customer.id, site.name); + if(!Directory.Exists(cache)) { + Directory.CreateDirectory(cache); + } + + LetsEncryptClient client = new LetsEncryptClient(settings.url, cache); + + //1. Client initialization + Console.WriteLine("1. Client Initialization..."); + client.Init(customer.contacts).Wait(); + Console.WriteLine(string.Format("Terms of service: {0}", client.GetTermsOfServiceUri())); + + //create folder for ssl + string ssl = Path.Combine(settings.ssl, 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 + CachedCertificateResult certRes = new CachedCertificateResult(); + if (client.TryGetCachedCertificate(site.hosts, out certRes)) + { + string cert = Path.Combine(ssl, site.name + ".crt"); + if(!File.Exists(cert)) + File.WriteAllText(cert, certRes.Certificate); + + string key = Path.Combine(ssl, site.name + ".key"); + if(!File.Exists(key)) { + using (StreamWriter writer = File.CreateText(key)) + Library.ExportPrivateKey(certRes.PrivateKey, writer); + } + + Console.WriteLine("Certificate and Key exists and valid."); + } + else { + if(!Directory.Exists(Path.Combine(settings.www, site.name))) { + throw new DirectoryNotFoundException(string.Format("Site {0} wasn't initialized", site.name)); + } + + //new nonce + client.NewNonce().Wait(); + + //try to make new order + try + { + //create new orders + Console.WriteLine("2. Client New Order..."); + Task> orders = client.NewOrder(site.hosts, site.challenge); + orders.Wait(); + + switch(site.challenge) { + case "http-01": { + //ensure to enable static file discovery on server in .well-known/acme-challenge + //and listen on 80 port + + //create acme directory for web site + string acme = Path.Combine(settings.www, site.name, settings.acme); + if(!Directory.Exists(acme)) { + Directory.CreateDirectory(acme); + } + + foreach (FileInfo file in new DirectoryInfo(acme).GetFiles()) + file.Delete(); + + foreach (var result in orders.Result) + { + Console.WriteLine("Key: " + result.Key + Environment.NewLine + "Value: " + result.Value); + string[] splitToken = result.Value.Split('~'); + + string token = Path.Combine(acme, splitToken[0]); + File.WriteAllText(token, splitToken[1]); + + //for Selinux on centos7 + Console.WriteLine(Library.RestoreCon(token)); + } + + + + break; + } + + case "dns-01": { + //Manage DNS server MX record, depends from provider + + break; + } + + default: { + + break; + } + } + + //complete challanges + Console.WriteLine("3. Client Complete Challange..."); + client.CompleteChallenges().Wait(); + Console.WriteLine("Challanges comleted."); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message.ToString()); + client.GetOrder(site.hosts).Wait(); + } - // get cached certificate and chech if it's valid - CachedCertificateResult certRes = new CachedCertificateResult(); - if (client.TryGetCachedCertificate(hosts, out certRes)) - { - File.WriteAllText(Path.Combine(certsPath, "maks-it.com.crt"), certRes.Certificate); + // Download new certificate + Console.WriteLine("4. Download certificate..."); + client.GetCertificate().Wait(); - using (StreamWriter writer = File.CreateText(Path.Combine(certsPath, "maks-it.com.key"))) - Library.ExportPrivateKey(certRes.PrivateKey, writer); - } - else { + // Write to filesystem + certRes = new CachedCertificateResult(); + if (client.TryGetCachedCertificate(site.hosts, out certRes)) { + string cert = Path.Combine(ssl, site.name + ".crt"); + File.WriteAllText(cert, certRes.Certificate); + + string key = Path.Combine(ssl, site.name + ".key"); + using (StreamWriter writer = File.CreateText(key)) + Library.ExportPrivateKey(certRes.PrivateKey, writer); - client.NewNonce().Wait(); + Console.WriteLine("Certificate saved."); + } + else { + Console.WriteLine("Unable to get new cached certificate."); + } - // 2 - try - { - Console.WriteLine("2. Client New Order..."); - Task> orders = client.NewOrder(hosts.ToArray(), "http-01"); - orders.Wait(); - - foreach (var result in orders.Result) - { - Console.WriteLine("Key: " + result.Key + Environment.NewLine + "Value: " + result.Value); - string[] splitToken = result.Value.Split('~'); - File.WriteAllText(FS.Path.Combine(tokensPath, splitToken[0]), splitToken[1]); + + } + } + catch (Exception ex) { + Console.WriteLine(ex.Message.ToString()); + } } - - // 3 - Console.WriteLine("3. Client Complete Challange..."); - client.CompleteChallenges().Wait(); - Console.WriteLine("Challanges comleted."); } - catch (Exception ex) - { + catch (Exception ex) { Console.WriteLine(ex.Message.ToString()); - client.GetOrder(hosts.ToArray()).Wait(); } - - - // 4 Download certificate - Console.WriteLine("4. Download certificate..."); - client.GetCertificate().Wait(); - - - // 5 Write to filesystem - //CachedCertificateResult certRes = new CachedCertificateResult(); - //if (client.TryGetCachedCertificate(hosts, out certRes)) { - // File.WriteAllText(Path.Combine(certsPath, "maks-it.com.crt"), certRes.Certificate); - - // using (StreamWriter writer = File.CreateText(Path.Combine(certsPath, "maks-it.com.key"))) - // Library.ExportPrivateKey(certRes.PrivateKey, writer); - //} - - Console.WriteLine("Certificate saved."); } - - - } catch (Exception ex) { Console.WriteLine(ex.Message.ToString()); } - - - - - Console.Read(); } - - - } - - } diff --git a/LetsEncrypt/SettingsProvider.cs b/LetsEncrypt/SettingsProvider.cs new file mode 100644 index 0000000..07b17b3 --- /dev/null +++ b/LetsEncrypt/SettingsProvider.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using Newtonsoft.Json; + +namespace LetsEncrypt +{ + + + public class SettingsProvider + { + private readonly string _path; + public Settings settings; + public SettingsProvider(string path) { + _path = path ?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json"); + + if(!File.Exists(_path)) + throw new FileNotFoundException(string.Format("Settings file \"{0}\" not found."), _path); + + settings = JsonConvert.DeserializeObject(File.ReadAllText(_path)); + } + } + + public class Settings { + public string url { get; set; } + public string www { get; set; } + public string acme { get; set; } + public string ssl { get; set; } + + public Customer [] customers { get; set;} + } + + public class Customer { + public string id { get; set; } + public string [] contacts { get; set; } + public string name { get; set; } + public string lastname { get; set; } + public Site [] sites { get; set; } + } + + public class Site { + public string name { get; set; } + public string [] hosts { get; set; } + public string challenge { get; set; } + } + + + +} \ No newline at end of file diff --git a/LetsEncrypt/settings.json b/LetsEncrypt/settings.json new file mode 100644 index 0000000..c082826 --- /dev/null +++ b/LetsEncrypt/settings.json @@ -0,0 +1,58 @@ +{ + "_StagingV2": "https://acme-staging-v02.api.letsencrypt.org/directory", + "_ProductionV2": "https://acme-v02.api.letsencrypt.org/directory", + + "url": "https://acme-staging-v02.api.letsencrypt.org/directory", + + "cache": "/home/maksym/Desktop/LetsEncrypt_Cache/cache", + + "www": "/var/www", + "acme": ".well-known/acme-challenge", + "ssl": "/etc/nginx/ssl", + + "customers": [ + { + "id": "{9b4c8584-dc83-4388-b45f-2942e34dca9d}", + "contacts": [ "maksym.sadovnychyy@gmail.com" ], + "name": "Maksym", + "lastname": "Sadovnychyy", + + "sites": [ + { + "name": "maks-it.com", + "hosts": [ + "maks-it.com", + "www.maks-it.com" + ], + "challenge": "http-01" + }, + { + "name": "maks-it.com", + "hosts": [ + "api.maks-it.com", + "www.api.maks-it.com" + ], + "challenge": "http-01" + } + ] + }, + { + "id": "{d6be989c-3b68-480d-9f4f-b7317674847a}", + "contacts": [ "anastasiia.pavlovskaia@gmail.com" ], + + "name": "Anastasiia", + "lastname": "Pavlovskaia", + + "sites": [ + { + "name": "nastyarey.com", + "hosts": [ + "nastyarey.com", + "www.nastyarey.com" + ], + "challenge": "http-01" + } + ] + } + ] +}