From 16c8d6875951f214bb6b1014dc50623a0a695dbb Mon Sep 17 00:00:00 2001 From: rkerber Date: Fri, 5 Feb 2016 15:15:29 -0600 Subject: [PATCH 1/4] Adding IISSiteServerPlugin Lots of work to get multiple site/ entire server certs. --- letsencrypt-win-simple/Plugin/IISPlugin.cs | 7 + .../Plugin/IISSiteServerPlugin.cs | 200 ++++++++++++++++++ letsencrypt-win-simple/Plugin/ManualPlugin.cs | 6 + letsencrypt-win-simple/Plugin/Plugin.cs | 6 + letsencrypt-win-simple/Program.cs | 9 +- .../letsencrypt-win-simple.csproj | 1 + 6 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 letsencrypt-win-simple/Plugin/IISSiteServerPlugin.cs diff --git a/letsencrypt-win-simple/Plugin/IISPlugin.cs b/letsencrypt-win-simple/Plugin/IISPlugin.cs index d272f14cd..1a419764d 100644 --- a/letsencrypt-win-simple/Plugin/IISPlugin.cs +++ b/letsencrypt-win-simple/Plugin/IISPlugin.cs @@ -319,6 +319,13 @@ public Version GetIisVersion() } } + public override void Renew(Target target) + { + // TODO: make a system where they can execute a program/batch file to update whatever they need after install. + // This method with just the Target paramater is currently only used by Centralized SSL + Console.WriteLine(" WARNING: Unable to renew."); + } + Site GetSite(Target target, ServerManager iisManager) { foreach (var site in iisManager.Sites) diff --git a/letsencrypt-win-simple/Plugin/IISSiteServerPlugin.cs b/letsencrypt-win-simple/Plugin/IISSiteServerPlugin.cs new file mode 100644 index 000000000..e59389f77 --- /dev/null +++ b/letsencrypt-win-simple/Plugin/IISSiteServerPlugin.cs @@ -0,0 +1,200 @@ +using Microsoft.Web.Administration; +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using Serilog; +using System.Text.RegularExpressions; + +namespace LetsEncrypt.ACME.Simple +{ + public class IISSiteServerPlugin : Plugin + { + public override string Name => "IISSiteServer"; + //This plugin is designed to allow a user to select multiple sites for a single SAN certificate or to generate a single SAN certificate for the entire server. + //This has seperate code from the main main Program.cs + + public override List GetTargets() + { + var result = new List(); + + return result; + } + + public override List GetSites() + { + var result = new List(); + + return result; + } + + public override void Install(Target target, string pfxFilename, X509Store store, X509Certificate2 certificate) + { + // TODO: make a system where they can execute a program/batch file to update whatever they need after install. + Console.WriteLine(" WARNING: Unable to configure server software."); + } + public override void Install(Target target) + { + // TODO: make a system where they can execute a program/batch file to update whatever they need after install. + // This method with just the Target paramater is currently only used by Centralized SSL + Console.WriteLine(" WARNING: Unable to configure server software."); + } + + public override void PrintMenu() + { + + Console.WriteLine(" S: Generate a single SAN certificate for multiple sites."); + } + + public override void HandleMenuResponse(string response, List targets) + { + if (response == "s") + { + Console.WriteLine("Running IISSiteServer Plugin"); + Log.Information("Running IISSiteServer Plugin"); + if (Program.Options.SAN) + { + List RunSites = new List(); + + Console.WriteLine("Enter all Site IDs comma seperated."); + Console.Write("S: for all sites on the server"); + var SANInput = Console.ReadLine(); + if (SANInput == "s") + { + RunSites.AddRange(targets); + } + else + { + string[] siteIDs = SANInput.Split(','); + + foreach (var id in siteIDs) + { + RunSites.AddRange(targets.Where(t => t.SiteId.ToString() == id)); + } + } + int hostCount = 0; + foreach(var site in RunSites) + { + hostCount = hostCount + site.AlternativeNames.Count(); + } + + if(hostCount > 100) + { + Console.WriteLine($" You have too many hosts for a SAN certificate. Let's Encrypt currently has a maximum of 100 alternative names per certificate."); + Log.Error("You have too many hosts for a SAN certificate. Let's Encrypt currently has a maximum of 100 alternative names per certificate."); + Environment.Exit(1); + } + + Target TotalTarget = CreateTarget(RunSites); + + ProcessTotaltarget(TotalTarget, RunSites); + } + else + { + Console.WriteLine($"Please run the application with --san to generate a SAN certificate"); + Log.Error("Please run the application with --san to generate a SAN certificate"); + } + } + } + public override void Renew(Target target) + { + List RunSites = new List(); + List targets = new List(); + + foreach (var plugin in Target.Plugins.Values) + { + if (plugin.Name == "IIS") + { + targets.AddRange(plugin.GetSites()); + } + } + + string[] siteIDs = target.Host.Split(','); + foreach (var id in siteIDs) + { + RunSites.AddRange(targets.Where(t => t.SiteId.ToString() == id)); + } + + Target TotalTarget = CreateTarget(RunSites); + + ProcessTotaltarget(TotalTarget, RunSites); + + } + + private Target CreateTarget(List RunSites) + { + Target TotalTarget = new Target(); + TotalTarget.PluginName = Name; + TotalTarget.SiteId = 0; + TotalTarget.WebRootPath = ""; + TotalTarget.Host = ""; + + foreach (var site in RunSites) + { + var auth = Program.Authorize(site); + if (auth.Status != "valid") + { + Console.WriteLine("All hosts under all sites need to pass authorization before you can continue"); + Log.Error("All hosts under all sites need to pass authorization before you can continue."); + Environment.Exit(1); + } + else + { + TotalTarget.Host = String.Format("{0},{1}", TotalTarget.Host, site.SiteId); + TotalTarget.AlternativeNames.AddRange(site.AlternativeNames); + } + } + return TotalTarget; + } + + private void ProcessTotaltarget(Target TotalTarget, List RunSites) + { + var pfxFilename = Program.GetCertificate(TotalTarget); + X509Store store; + X509Certificate2 certificate; + if (!Program.CentralSSL) + { + Log.Information("Installing Non-Central SSL Certificate in the certificate store"); + Program.InstallCertificate(TotalTarget, pfxFilename, out store, out certificate); + if (Program.Options.Test && !Program.Options.Renew) + { + Console.WriteLine($"\nDo you want to add/update the certificate to your server software? (Y/N) "); + if (!Program.PromptYesNo()) + return; + } + Log.Information("Installing Non-Central SSL Certificate in server software"); + foreach (var site in RunSites) + { + site.Plugin.Install(site, pfxFilename, store, certificate); + } + } + else if (!Program.Options.Renew) + { + //If it is using centralized SSL and renewing, it doesn't need to change the + //binding since just the certificate needs to be updated at the central ssl path + Log.Information("Updating new Central SSL Certificate"); + foreach (var site in RunSites) + { + site.Plugin.Install(site); + } + } + + if (Program.Options.Test && !Program.Options.Renew) + { + Console.WriteLine($"\nDo you want to automatically renew this certificate in 60 days? This will add a task scheduler task. (Y/N) "); + if (!Program.PromptYesNo()) + return; + } + + if (!Program.Options.Renew) + { + Log.Information("Adding renewal for {binding}", TotalTarget); + Program.ScheduleRenewal(TotalTarget); + } + } + } +} diff --git a/letsencrypt-win-simple/Plugin/ManualPlugin.cs b/letsencrypt-win-simple/Plugin/ManualPlugin.cs index eb97fd957..b6b9f6e7c 100644 --- a/letsencrypt-win-simple/Plugin/ManualPlugin.cs +++ b/letsencrypt-win-simple/Plugin/ManualPlugin.cs @@ -37,6 +37,12 @@ public override void Install(Target target) // This method with just the Target paramater is currently only used by Centralized SSL Console.WriteLine(" WARNING: Unable to configure server software."); } + public override void Renew(Target target) + { + // TODO: make a system where they can execute a program/batch file to update whatever they need after install. + // This method with just the Target paramater is currently only used by Centralized SSL + Console.WriteLine(" WARNING: Unable to renew."); + } public override void PrintMenu() { diff --git a/letsencrypt-win-simple/Plugin/Plugin.cs b/letsencrypt-win-simple/Plugin/Plugin.cs index 4dc0966b0..781bac38b 100644 --- a/letsencrypt-win-simple/Plugin/Plugin.cs +++ b/letsencrypt-win-simple/Plugin/Plugin.cs @@ -69,5 +69,11 @@ public virtual void OnAuthorizeFail(Target target) { } /// /// public abstract void Install(Target target); + + /// + /// Should renew the certificate + /// + /// + public abstract void Renew(Target target); } } diff --git a/letsencrypt-win-simple/Program.cs b/letsencrypt-win-simple/Program.cs index eab174bbc..6ce0ca24d 100644 --- a/letsencrypt-win-simple/Program.cs +++ b/letsencrypt-win-simple/Program.cs @@ -750,7 +750,14 @@ public static void CheckRenewals() //Not using SAN Options.SAN = false; } - Auto(renewal.Binding); + if (renewal.Binding.PluginName == "IIS") + { + Auto(renewal.Binding); + } + else + { + renewal.Binding.Plugin.Renew(renewal.Binding); + } renewal.Date = DateTime.UtcNow.AddDays(renewalPeriod); settings.SaveRenewals(renewals); diff --git a/letsencrypt-win-simple/letsencrypt-win-simple.csproj b/letsencrypt-win-simple/letsencrypt-win-simple.csproj index efd2d4b95..05e4844a7 100644 --- a/letsencrypt-win-simple/letsencrypt-win-simple.csproj +++ b/letsencrypt-win-simple/letsencrypt-win-simple.csproj @@ -131,6 +131,7 @@ + From c7ebfd76cb686770301a13ef503c29a22e96c6f5 Mon Sep 17 00:00:00 2001 From: rkerber Date: Sun, 7 Feb 2016 00:15:02 -0600 Subject: [PATCH 2/4] Got Multi Site SAN Working --- letsencrypt-win-simple/Plugin/IISPlugin.cs | 45 +++++++++-------- .../Plugin/IISSiteServerPlugin.cs | 49 +++++++++++-------- 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/letsencrypt-win-simple/Plugin/IISPlugin.cs b/letsencrypt-win-simple/Plugin/IISPlugin.cs index 1a419764d..8d0c11dd9 100644 --- a/letsencrypt-win-simple/Plugin/IISPlugin.cs +++ b/letsencrypt-win-simple/Plugin/IISPlugin.cs @@ -194,7 +194,6 @@ public override void Install(Target target, string pfxFilename, X509Store store, hosts.AddRange(target.AlternativeNames); } } - foreach (var host in hosts) { var existingBinding = (from b in site.Bindings where b.Host == host && b.Protocol == "https" select b).FirstOrDefault(); @@ -210,19 +209,22 @@ public override void Install(Target target, string pfxFilename, X509Store store, Console.WriteLine($" Adding https Binding"); Log.Information("Adding https Binding"); var existingHTTPBinding = (from b in site.Bindings where b.Host == host && b.Protocol == "http" select b).FirstOrDefault(); - string HTTPEndpoint = existingHTTPBinding.EndPoint.ToString(); - string IP = HTTPEndpoint.Remove(HTTPEndpoint.IndexOf(':'), (HTTPEndpoint.Length - HTTPEndpoint.IndexOf(':'))); - - if (IP == "0.0.0.0") + if (existingHTTPBinding != null) //This is a fix for the multiple site SAN cert { - IP = ""; //Remove the IP if it is 0.0.0.0 That happens if an IP wasn't set on the HTTP site and it used any available IP - } + string HTTPEndpoint = existingHTTPBinding.EndPoint.ToString(); + string IP = HTTPEndpoint.Remove(HTTPEndpoint.IndexOf(':'), (HTTPEndpoint.Length - HTTPEndpoint.IndexOf(':'))); + + if (IP == "0.0.0.0") + { + IP = ""; //Remove the IP if it is 0.0.0.0 That happens if an IP wasn't set on the HTTP site and it used any available IP + } - var iisBinding = site.Bindings.Add(IP + ":443:" + host, certificate.GetCertHash(), store.Name); - iisBinding.Protocol = "https"; + var iisBinding = site.Bindings.Add(IP + ":443:" + host, certificate.GetCertHash(), store.Name); + iisBinding.Protocol = "https"; - if (iisVersion.Major >= 8) - iisBinding.SetAttributeValue("sslFlags", 1); // Enable SNI support + if (iisVersion.Major >= 8) + iisBinding.SetAttributeValue("sslFlags", 1); // Enable SNI support + } } } Console.WriteLine($" Committing binding changes to IIS"); @@ -273,18 +275,21 @@ public override void Install(Target target) Console.WriteLine($" Adding Central SSL https Binding"); Log.Information("Adding Central SSL https Binding"); var existingHTTPBinding = (from b in site.Bindings where b.Host == host && b.Protocol == "http" select b).FirstOrDefault(); - string HTTPEndpoint = existingHTTPBinding.EndPoint.ToString(); - string IP = HTTPEndpoint.Remove(HTTPEndpoint.IndexOf(':'), (HTTPEndpoint.Length - HTTPEndpoint.IndexOf(':'))); - - if (IP == "0.0.0.0") + if (existingHTTPBinding != null) //This is a fix for the multiple site SAN cert { - IP = ""; //Remove the IP if it is 0.0.0.0 That happens if an IP wasn't set on the HTTP site and it used any available IP - } + string HTTPEndpoint = existingHTTPBinding.EndPoint.ToString(); + string IP = HTTPEndpoint.Remove(HTTPEndpoint.IndexOf(':'), (HTTPEndpoint.Length - HTTPEndpoint.IndexOf(':'))); - var iisBinding = site.Bindings.Add(IP + ":443:" + host, "https"); + if (IP == "0.0.0.0") + { + IP = ""; //Remove the IP if it is 0.0.0.0 That happens if an IP wasn't set on the HTTP site and it used any available IP + } - if (iisVersion.Major >= 8) - iisBinding.SetAttributeValue("sslFlags", 2); // Enable Centralized Certificate Store + var iisBinding = site.Bindings.Add(IP + ":443:" + host, "https"); + + if (iisVersion.Major >= 8) + iisBinding.SetAttributeValue("sslFlags", 2); // Enable Centralized Certificate Store + } } } Console.WriteLine($" Committing binding changes to IIS"); diff --git a/letsencrypt-win-simple/Plugin/IISSiteServerPlugin.cs b/letsencrypt-win-simple/Plugin/IISSiteServerPlugin.cs index e59389f77..a126c6904 100644 --- a/letsencrypt-win-simple/Plugin/IISSiteServerPlugin.cs +++ b/letsencrypt-win-simple/Plugin/IISSiteServerPlugin.cs @@ -5,10 +5,7 @@ using System.IO; using System.Linq; using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading.Tasks; using Serilog; -using System.Text.RegularExpressions; namespace LetsEncrypt.ACME.Simple { @@ -46,8 +43,10 @@ public override void Install(Target target) public override void PrintMenu() { - - Console.WriteLine(" S: Generate a single SAN certificate for multiple sites."); + if (Program.Options.SAN) + { + Console.WriteLine(" S: Generate a single SAN certificate for multiple sites."); + } } public override void HandleMenuResponse(string response, List targets) @@ -58,26 +57,25 @@ public override void HandleMenuResponse(string response, List targets) Log.Information("Running IISSiteServer Plugin"); if (Program.Options.SAN) { - List RunSites = new List(); + List SiteList = new List(); - Console.WriteLine("Enter all Site IDs comma seperated."); - Console.Write("S: for all sites on the server"); + Console.WriteLine("Enter all Site IDs seperated by a comma"); + Console.Write(" S: for all sites on the server "); var SANInput = Console.ReadLine(); if (SANInput == "s") { - RunSites.AddRange(targets); + SiteList.AddRange(targets); } else { string[] siteIDs = SANInput.Split(','); - foreach (var id in siteIDs) { - RunSites.AddRange(targets.Where(t => t.SiteId.ToString() == id)); + SiteList.AddRange(targets.Where(t => t.SiteId.ToString() == id)); } } int hostCount = 0; - foreach(var site in RunSites) + foreach (var site in SiteList) { hostCount = hostCount + site.AlternativeNames.Count(); } @@ -88,10 +86,8 @@ public override void HandleMenuResponse(string response, List targets) Log.Error("You have too many hosts for a SAN certificate. Let's Encrypt currently has a maximum of 100 alternative names per certificate."); Environment.Exit(1); } - - Target TotalTarget = CreateTarget(RunSites); - - ProcessTotaltarget(TotalTarget, RunSites); + Target TotalTarget = CreateTarget(SiteList); + ProcessTotaltarget(TotalTarget, SiteList); } else { @@ -120,7 +116,6 @@ public override void Renew(Target target) } Target TotalTarget = CreateTarget(RunSites); - ProcessTotaltarget(TotalTarget, RunSites); } @@ -131,7 +126,6 @@ private Target CreateTarget(List RunSites) TotalTarget.PluginName = Name; TotalTarget.SiteId = 0; TotalTarget.WebRootPath = ""; - TotalTarget.Host = ""; foreach (var site in RunSites) { @@ -144,8 +138,23 @@ private Target CreateTarget(List RunSites) } else { - TotalTarget.Host = String.Format("{0},{1}", TotalTarget.Host, site.SiteId); - TotalTarget.AlternativeNames.AddRange(site.AlternativeNames); + if (TotalTarget.Host == null) + { + TotalTarget.Host = site.SiteId.ToString(); + + } + else + { + TotalTarget.Host = String.Format("{0},{1}", TotalTarget.Host, site.SiteId); + } + if (TotalTarget.AlternativeNames == null) + { + TotalTarget.AlternativeNames = site.AlternativeNames; + } + else + { + TotalTarget.AlternativeNames.AddRange(site.AlternativeNames); + } } } return TotalTarget; From ba6bf6265a06fc06f233c54ef3ce80e363b9d312 Mon Sep 17 00:00:00 2001 From: rkerber Date: Mon, 8 Feb 2016 14:04:41 -0600 Subject: [PATCH 3/4] SAN/ Cleanup/ RenewalDays/ CertificateStore Work Added RenewalDays config option Added CertificateStore config option fixes #98 Fixed some variable names, locations, and protection Added ObjectExtentions.cs to be able to copy objects so they don't all get updated. Changed Manual Plugin sanInput to be able to have a longer input fixes #102 Fixed Multiple Site SAN Issue where it was trying to update bindings that were not there. --- letsencrypt-win-simple/App.config | 8 +- letsencrypt-win-simple/ObjectExtensions.cs | 132 ++++++++++ letsencrypt-win-simple/Options.cs | 12 +- letsencrypt-win-simple/Plugin/IISPlugin.cs | 51 ++-- .../Plugin/IISSiteServerPlugin.cs | 89 ++++--- letsencrypt-win-simple/Plugin/ManualPlugin.cs | 15 +- letsencrypt-win-simple/Plugin/Plugin.cs | 2 +- letsencrypt-win-simple/Program.cs | 249 ++++++++++-------- .../Properties/AssemblyInfo.cs | 2 +- .../Properties/Settings.Designer.cs | 18 ++ .../Properties/Settings.settings | 6 + letsencrypt-win-simple/ScheduledRenewal.cs | 4 +- .../letsencrypt-win-simple.csproj | 1 + 13 files changed, 393 insertions(+), 196 deletions(-) create mode 100644 letsencrypt-win-simple/ObjectExtensions.cs diff --git a/letsencrypt-win-simple/App.config b/letsencrypt-win-simple/App.config index 6452d7560..692f8ce0e 100644 --- a/letsencrypt-win-simple/App.config +++ b/letsencrypt-win-simple/App.config @@ -28,7 +28,13 @@ 50 - + + + + 60 + + + WebHosting diff --git a/letsencrypt-win-simple/ObjectExtensions.cs b/letsencrypt-win-simple/ObjectExtensions.cs new file mode 100644 index 000000000..0a978063f --- /dev/null +++ b/letsencrypt-win-simple/ObjectExtensions.cs @@ -0,0 +1,132 @@ +using System.Collections.Generic; +using System.Reflection; +using System.ArrayExtensions; + +namespace System +{ + //From https://github.com/Burtsev-Alexey/net-object-deep-copy + public static class ObjectExtensions + { + private static readonly MethodInfo CloneMethod = typeof(Object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance); + + public static bool IsPrimitive(this Type type) + { + if (type == typeof(String)) return true; + return (type.IsValueType & type.IsPrimitive); + } + + public static Object Copy(this Object originalObject) + { + return InternalCopy(originalObject, new Dictionary(new ReferenceEqualityComparer())); + } + private static Object InternalCopy(Object originalObject, IDictionary visited) + { + if (originalObject == null) return null; + var typeToReflect = originalObject.GetType(); + if (IsPrimitive(typeToReflect)) return originalObject; + if (visited.ContainsKey(originalObject)) return visited[originalObject]; + if (typeof(Delegate).IsAssignableFrom(typeToReflect)) return null; + var cloneObject = CloneMethod.Invoke(originalObject, null); + if (typeToReflect.IsArray) + { + var arrayType = typeToReflect.GetElementType(); + if (IsPrimitive(arrayType) == false) + { + Array clonedArray = (Array)cloneObject; + clonedArray.ForEach((array, indices) => array.SetValue(InternalCopy(clonedArray.GetValue(indices), visited), indices)); + } + + } + visited.Add(originalObject, cloneObject); + CopyFields(originalObject, visited, cloneObject, typeToReflect); + RecursiveCopyBaseTypePrivateFields(originalObject, visited, cloneObject, typeToReflect); + return cloneObject; + } + + private static void RecursiveCopyBaseTypePrivateFields(object originalObject, IDictionary visited, object cloneObject, Type typeToReflect) + { + if (typeToReflect.BaseType != null) + { + RecursiveCopyBaseTypePrivateFields(originalObject, visited, cloneObject, typeToReflect.BaseType); + CopyFields(originalObject, visited, cloneObject, typeToReflect.BaseType, BindingFlags.Instance | BindingFlags.NonPublic, info => info.IsPrivate); + } + } + + private static void CopyFields(object originalObject, IDictionary visited, object cloneObject, Type typeToReflect, BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy, Func filter = null) + { + foreach (FieldInfo fieldInfo in typeToReflect.GetFields(bindingFlags)) + { + if (filter != null && filter(fieldInfo) == false) continue; + if (IsPrimitive(fieldInfo.FieldType)) continue; + var originalFieldValue = fieldInfo.GetValue(originalObject); + var clonedFieldValue = InternalCopy(originalFieldValue, visited); + fieldInfo.SetValue(cloneObject, clonedFieldValue); + } + } + public static T Copy(this T original) + { + return (T)Copy((Object)original); + } + } + + public class ReferenceEqualityComparer : EqualityComparer + { + public override bool Equals(object x, object y) + { + return ReferenceEquals(x, y); + } + public override int GetHashCode(object obj) + { + if (obj == null) return 0; + return obj.GetHashCode(); + } + } + + namespace ArrayExtensions + { + public static class ArrayExtensions + { + public static void ForEach(this Array array, Action action) + { + if (array.LongLength == 0) return; + ArrayTraverse walker = new ArrayTraverse(array); + do action(array, walker.Position); + while (walker.Step()); + } + } + + internal class ArrayTraverse + { + public int[] Position; + private int[] maxLengths; + + public ArrayTraverse(Array array) + { + maxLengths = new int[array.Rank]; + for (int i = 0; i < array.Rank; ++i) + { + maxLengths[i] = array.GetLength(i) - 1; + } + Position = new int[array.Rank]; + } + + public bool Step() + { + for (int i = 0; i < Position.Length; ++i) + { + if (Position[i] < maxLengths[i]) + { + Position[i]++; + for (int j = 0; j < i; j++) + { + Position[j] = 0; + } + return true; + } + } + return false; + } + } + } + +} \ No newline at end of file diff --git a/letsencrypt-win-simple/Options.cs b/letsencrypt-win-simple/Options.cs index b9fcf590b..0a241c1e8 100644 --- a/letsencrypt-win-simple/Options.cs +++ b/letsencrypt-win-simple/Options.cs @@ -10,15 +10,15 @@ namespace LetsEncrypt.ACME.Simple class Options { [Option(Default = "https://acme-v01.api.letsencrypt.org/", HelpText = "The address of the ACME server to use.")] - public string BaseURI { get; set; } + public string BaseUri { get; set; } [Option(HelpText = "Accept the terms of service.")] - public bool AcceptTOS { get; set; } + public bool AcceptTos { get; set; } [Option(HelpText = "Check for renewals.")] public bool Renew { get; set; } - [Option(HelpText = "Overrides BaseURI setting to https://acme-staging.api.letsencrypt.org/")] + [Option(HelpText = "Overrides BaseUri setting to https://acme-staging.api.letsencrypt.org/")] public bool Test { get; set; } [Option(HelpText = "A host name to manually get a certificate for. --webroot must also be set.")] @@ -28,13 +28,13 @@ class Options public string WebRoot { get; set; } [Option(HelpText = "Path for Centralized Certificate Store (This enables Centralized SSL). Ex. \\\\storage\\central_ssl\\")] - public string CentralSSLStore { get; set; } + public string CentralSslStore { get; set; } [Option(HelpText = "Hide sites that have existing HTTPS bindings")] - public bool HideHTTPS { get; set; } + public bool HideHttps { get; set; } [Option(HelpText = "Certificates per site instead of per host")] - public bool SAN { get; set; } + public bool San { get; set; } // can't easily make this a command line option since it would have to be saved //[Option(Default = 60f, HelpText = "Renewal period in days. Can be set to negative to test.")] diff --git a/letsencrypt-win-simple/Plugin/IISPlugin.cs b/letsencrypt-win-simple/Plugin/IISPlugin.cs index 8d0c11dd9..2c7adcc0f 100644 --- a/letsencrypt-win-simple/Plugin/IISPlugin.cs +++ b/letsencrypt-win-simple/Plugin/IISPlugin.cs @@ -16,7 +16,7 @@ public class IISPlugin : Plugin { public override string Name => "IIS"; - static Version iisVersion; + private static Version _iisVersion; public override List GetTargets() { @@ -25,8 +25,8 @@ public override List GetTargets() var result = new List(); - iisVersion = GetIisVersion(); - if (iisVersion.Major == 0) + _iisVersion = GetIisVersion(); + if (_iisVersion.Major == 0) { Console.WriteLine(" IIS Version not found in windows registry. Skipping scan."); Log.Information("IIS Version not found in windows registry. Skipping scan."); @@ -62,7 +62,7 @@ public override List GetTargets() } siteHTTP.AddRange(returnHTTP); - if (Program.Options.HideHTTPS == true) + if (Program.Options.HideHttps == true) { foreach (var bindingHTTPS in siteHTTPS) { @@ -93,6 +93,7 @@ public override List GetTargets() return result; } + public override List GetSites() { @@ -101,8 +102,8 @@ public override List GetSites() var result = new List(); - iisVersion = GetIisVersion(); - if (iisVersion.Major == 0) + _iisVersion = GetIisVersion(); + if (_iisVersion.Major == 0) { Console.WriteLine(" IIS Version not found in windows registry. Skipping scan."); Log.Information("IIS Version not found in windows registry. Skipping scan."); @@ -114,29 +115,29 @@ public override List GetSites() foreach (var site in iisManager.Sites) { List returnHTTP = new List(); - List Hosts = new List(); + List hosts = new List(); foreach (var binding in site.Bindings) { //Get HTTP sites that aren't IDN if (!String.IsNullOrEmpty(binding.Host) && binding.Protocol == "http" && !Regex.IsMatch(binding.Host, @"[^\u0000-\u007F]")) { - if (Hosts.Where(h => h == binding.Host).Count() == 0) + if (hosts.Where(h => h == binding.Host).Count() == 0) { - Hosts.Add(binding.Host); + hosts.Add(binding.Host); } returnHTTP.Add(new Target() { SiteId = site.Id, Host = binding.Host, WebRootPath = site.Applications["/"].VirtualDirectories["/"].PhysicalPath, PluginName = Name }); } } - if (Hosts.Count <= 100) + if (hosts.Count <= 100) { - result.Add(new Target() { SiteId = site.Id, Host = site.Name, WebRootPath = site.Applications["/"].VirtualDirectories["/"].PhysicalPath, PluginName = Name, AlternativeNames = Hosts }); + result.Add(new Target() { SiteId = site.Id, Host = site.Name, WebRootPath = site.Applications["/"].VirtualDirectories["/"].PhysicalPath, PluginName = Name, AlternativeNames = hosts }); } else { Console.WriteLine($" {site.Name} has too many hosts for a SAN certificate. Let's Encrypt currently has a maximum of 100 alternative names per certificate."); - Log.Error("{Name} has too many hosts for a SAN certificate. Let's Encrypt currently has a maximum of 100 alternative names per certificate.", site.Name); + Log.Error("{Name} has too many hosts for a San certificate. Let's Encrypt currently has a maximum of 100 alternative names per certificate.", site.Name); } } } @@ -151,7 +152,7 @@ public override List GetSites() return result; } - string sourceFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "web_config.xml"); + private readonly string _sourceFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "web_config.xml"); public override void BeforeAuthorize(Target target, string answerPath) { @@ -160,7 +161,7 @@ public override void BeforeAuthorize(Target target, string answerPath) Console.WriteLine($" Writing web.config to add extensionless mime type to {webConfigPath}"); Log.Information("Writing web.config to add extensionless mime type to {webConfigPath}", webConfigPath); - File.Copy(sourceFilePath, webConfigPath, true); + File.Copy(_sourceFilePath, webConfigPath, true); } public override void OnAuthorizeFail(Target target) @@ -173,8 +174,8 @@ 1. In IIS manager goto Site/Server->Handler Mappings->View Ordered List 2. Move the StaticFile mapping above the ExtensionlessUrlHandler mappings. (like this http://i.stack.imgur.com/nkvrL.png) 3. If you need to make changes to your web.config file, update the one -at " + sourceFilePath); - Log.Error("Authorize failed: This could be caused by IIS not being setup to handle extensionless static files.Here's how to fix that: 1.In IIS manager goto Site/ Server->Handler Mappings->View Ordered List 2.Move the StaticFile mapping above the ExtensionlessUrlHandler mappings. (like this http://i.stack.imgur.com/nkvrL.png) 3.If you need to make changes to your web.config file, update the one at {sourceFilePath}", sourceFilePath); +at " + _sourceFilePath); + Log.Error("Authorize failed: This could be caused by IIS not being setup to handle extensionless static files.Here's how to fix that: 1.In IIS manager goto Site/ Server->Handler Mappings->View Ordered List 2.Move the StaticFile mapping above the ExtensionlessUrlHandler mappings. (like this http://i.stack.imgur.com/nkvrL.png) 3.If you need to make changes to your web.config file, update the one at {_sourceFilePath}", _sourceFilePath); } public override void Install(Target target, string pfxFilename, X509Store store, X509Certificate2 certificate) @@ -183,7 +184,7 @@ public override void Install(Target target, string pfxFilename, X509Store store, { var site = GetSite(target, iisManager); List hosts = new List(); - if (!Program.Options.SAN) + if (!Program.Options.San) { hosts.Add(target.Host); } @@ -209,7 +210,7 @@ public override void Install(Target target, string pfxFilename, X509Store store, Console.WriteLine($" Adding https Binding"); Log.Information("Adding https Binding"); var existingHTTPBinding = (from b in site.Bindings where b.Host == host && b.Protocol == "http" select b).FirstOrDefault(); - if (existingHTTPBinding != null) //This is a fix for the multiple site SAN cert + if (existingHTTPBinding != null) //This is a fix for the multiple site San cert { string HTTPEndpoint = existingHTTPBinding.EndPoint.ToString(); string IP = HTTPEndpoint.Remove(HTTPEndpoint.IndexOf(':'), (HTTPEndpoint.Length - HTTPEndpoint.IndexOf(':'))); @@ -222,7 +223,7 @@ public override void Install(Target target, string pfxFilename, X509Store store, var iisBinding = site.Bindings.Add(IP + ":443:" + host, certificate.GetCertHash(), store.Name); iisBinding.Protocol = "https"; - if (iisVersion.Major >= 8) + if (_iisVersion.Major >= 8) iisBinding.SetAttributeValue("sslFlags", 1); // Enable SNI support } } @@ -243,7 +244,7 @@ public override void Install(Target target) var site = GetSite(target, iisManager); List hosts = new List(); - if (!Program.Options.SAN) + if (!Program.Options.San) { hosts.Add(target.Host); } @@ -256,14 +257,14 @@ public override void Install(Target target) { Console.WriteLine($" Updating Existing https Binding"); Log.Information("Updating Existing https Binding"); - if (iisVersion.Major >= 8 && existingBinding.GetAttributeValue("sslFlags").ToString() != "2") + if (_iisVersion.Major >= 8 && existingBinding.GetAttributeValue("sslFlags").ToString() != "2") { //IIS 8+ and not using centralized SSL existingBinding.CertificateStoreName = null; existingBinding.CertificateHash = null; existingBinding.SetAttributeValue("sslFlags", 2); } - else if (!(iisVersion.Major >= 8)) + else if (!(_iisVersion.Major >= 8)) { Log.Error("You aren't using IIS 8 or greater, so centralized SSL is not supported"); //Not using IIS 8+ so can't set centralized certificates @@ -275,7 +276,7 @@ public override void Install(Target target) Console.WriteLine($" Adding Central SSL https Binding"); Log.Information("Adding Central SSL https Binding"); var existingHTTPBinding = (from b in site.Bindings where b.Host == host && b.Protocol == "http" select b).FirstOrDefault(); - if (existingHTTPBinding != null) //This is a fix for the multiple site SAN cert + if (existingHTTPBinding != null) //This is a fix for the multiple site San cert { string HTTPEndpoint = existingHTTPBinding.EndPoint.ToString(); string IP = HTTPEndpoint.Remove(HTTPEndpoint.IndexOf(':'), (HTTPEndpoint.Length - HTTPEndpoint.IndexOf(':'))); @@ -287,7 +288,7 @@ public override void Install(Target target) var iisBinding = site.Bindings.Add(IP + ":443:" + host, "https"); - if (iisVersion.Major >= 8) + if (_iisVersion.Major >= 8) iisBinding.SetAttributeValue("sslFlags", 2); // Enable Centralized Certificate Store } } @@ -331,7 +332,7 @@ public override void Renew(Target target) Console.WriteLine(" WARNING: Unable to renew."); } - Site GetSite(Target target, ServerManager iisManager) + private Site GetSite(Target target, ServerManager iisManager) { foreach (var site in iisManager.Sites) { diff --git a/letsencrypt-win-simple/Plugin/IISSiteServerPlugin.cs b/letsencrypt-win-simple/Plugin/IISSiteServerPlugin.cs index a126c6904..68346e454 100644 --- a/letsencrypt-win-simple/Plugin/IISSiteServerPlugin.cs +++ b/letsencrypt-win-simple/Plugin/IISSiteServerPlugin.cs @@ -12,7 +12,7 @@ namespace LetsEncrypt.ACME.Simple public class IISSiteServerPlugin : Plugin { public override string Name => "IISSiteServer"; - //This plugin is designed to allow a user to select multiple sites for a single SAN certificate or to generate a single SAN certificate for the entire server. + //This plugin is designed to allow a user to select multiple sites for a single San certificate or to generate a single San certificate for the entire server. //This has seperate code from the main main Program.cs public override List GetTargets() @@ -43,9 +43,9 @@ public override void Install(Target target) public override void PrintMenu() { - if (Program.Options.SAN) + if (Program.Options.San) { - Console.WriteLine(" S: Generate a single SAN certificate for multiple sites."); + Console.WriteLine(" S: Generate a single San certificate for multiple sites."); } } @@ -55,27 +55,27 @@ public override void HandleMenuResponse(string response, List targets) { Console.WriteLine("Running IISSiteServer Plugin"); Log.Information("Running IISSiteServer Plugin"); - if (Program.Options.SAN) + if (Program.Options.San) { - List SiteList = new List(); + List siteList = new List(); Console.WriteLine("Enter all Site IDs seperated by a comma"); Console.Write(" S: for all sites on the server "); - var SANInput = Console.ReadLine(); - if (SANInput == "s") + var sanInput = Console.ReadLine(); + if (sanInput == "s") { - SiteList.AddRange(targets); + siteList.AddRange(targets); } else { - string[] siteIDs = SANInput.Split(','); + string[] siteIDs = sanInput.Split(','); foreach (var id in siteIDs) { - SiteList.AddRange(targets.Where(t => t.SiteId.ToString() == id)); + siteList.AddRange(targets.Where(t => t.SiteId.ToString() == id)); } } int hostCount = 0; - foreach (var site in SiteList) + foreach (var site in siteList) { hostCount = hostCount + site.AlternativeNames.Count(); } @@ -83,22 +83,22 @@ public override void HandleMenuResponse(string response, List targets) if(hostCount > 100) { Console.WriteLine($" You have too many hosts for a SAN certificate. Let's Encrypt currently has a maximum of 100 alternative names per certificate."); - Log.Error("You have too many hosts for a SAN certificate. Let's Encrypt currently has a maximum of 100 alternative names per certificate."); + Log.Error("You have too many hosts for a San certificate. Let's Encrypt currently has a maximum of 100 alternative names per certificate."); Environment.Exit(1); } - Target TotalTarget = CreateTarget(SiteList); - ProcessTotaltarget(TotalTarget, SiteList); + Target totalTarget = CreateTarget(siteList); + ProcessTotaltarget(totalTarget, siteList); } else { Console.WriteLine($"Please run the application with --san to generate a SAN certificate"); - Log.Error("Please run the application with --san to generate a SAN certificate"); + Log.Error("Please run the application with --san to generate a San certificate"); } } } public override void Renew(Target target) { - List RunSites = new List(); + List runSites = new List(); List targets = new List(); foreach (var plugin in Target.Plugins.Values) @@ -112,22 +112,22 @@ public override void Renew(Target target) string[] siteIDs = target.Host.Split(','); foreach (var id in siteIDs) { - RunSites.AddRange(targets.Where(t => t.SiteId.ToString() == id)); + runSites.AddRange(targets.Where(t => t.SiteId.ToString() == id)); } - Target TotalTarget = CreateTarget(RunSites); - ProcessTotaltarget(TotalTarget, RunSites); + Target totalTarget = CreateTarget(runSites); + ProcessTotaltarget(totalTarget, runSites); } - private Target CreateTarget(List RunSites) + private Target CreateTarget(List sites) { - Target TotalTarget = new Target(); - TotalTarget.PluginName = Name; - TotalTarget.SiteId = 0; - TotalTarget.WebRootPath = ""; + Target totalTarget = new Target(); + totalTarget.PluginName = Name; + totalTarget.SiteId = 0; + totalTarget.WebRootPath = ""; - foreach (var site in RunSites) + foreach (var site in sites) { var auth = Program.Authorize(site); if (auth.Status != "valid") @@ -138,37 +138,40 @@ private Target CreateTarget(List RunSites) } else { - if (TotalTarget.Host == null) + if (totalTarget.Host == null) { - TotalTarget.Host = site.SiteId.ToString(); + totalTarget.Host = site.SiteId.ToString(); } else { - TotalTarget.Host = String.Format("{0},{1}", TotalTarget.Host, site.SiteId); + totalTarget.Host = String.Format("{0},{1}", totalTarget.Host, site.SiteId); } - if (TotalTarget.AlternativeNames == null) + if (totalTarget.AlternativeNames == null) { - TotalTarget.AlternativeNames = site.AlternativeNames; + Target altNames = site.Copy(); //Had to copy the object otherwise the alternative names for the site were being updated from Totaltarget. + totalTarget.AlternativeNames = altNames.AlternativeNames; } else { - TotalTarget.AlternativeNames.AddRange(site.AlternativeNames); + Target altNames = site.Copy(); //Had to copy the object otherwise the alternative names for the site were being updated from Totaltarget. + totalTarget.AlternativeNames.AddRange(altNames.AlternativeNames); } } } - return TotalTarget; + return totalTarget; } - private void ProcessTotaltarget(Target TotalTarget, List RunSites) + private static void ProcessTotaltarget(Target totalTarget, List runSites) { - var pfxFilename = Program.GetCertificate(TotalTarget); - X509Store store; - X509Certificate2 certificate; - if (!Program.CentralSSL) + + if (!Program.CentralSsl) { + var pfxFilename = Program.GetCertificate(totalTarget); + X509Store store; + X509Certificate2 certificate; Log.Information("Installing Non-Central SSL Certificate in the certificate store"); - Program.InstallCertificate(TotalTarget, pfxFilename, out store, out certificate); + Program.InstallCertificate(totalTarget, pfxFilename, out store, out certificate); if (Program.Options.Test && !Program.Options.Renew) { Console.WriteLine($"\nDo you want to add/update the certificate to your server software? (Y/N) "); @@ -176,7 +179,7 @@ private void ProcessTotaltarget(Target TotalTarget, List RunSites) return; } Log.Information("Installing Non-Central SSL Certificate in server software"); - foreach (var site in RunSites) + foreach (var site in runSites) { site.Plugin.Install(site, pfxFilename, store, certificate); } @@ -186,7 +189,7 @@ private void ProcessTotaltarget(Target TotalTarget, List RunSites) //If it is using centralized SSL and renewing, it doesn't need to change the //binding since just the certificate needs to be updated at the central ssl path Log.Information("Updating new Central SSL Certificate"); - foreach (var site in RunSites) + foreach (var site in runSites) { site.Plugin.Install(site); } @@ -194,15 +197,15 @@ private void ProcessTotaltarget(Target TotalTarget, List RunSites) if (Program.Options.Test && !Program.Options.Renew) { - Console.WriteLine($"\nDo you want to automatically renew this certificate in 60 days? This will add a task scheduler task. (Y/N) "); + Console.WriteLine($"\nDo you want to automatically renew this certificate in {Program.RenewalPeriod} days? This will add a task scheduler task. (Y/N) "); if (!Program.PromptYesNo()) return; } if (!Program.Options.Renew) { - Log.Information("Adding renewal for {binding}", TotalTarget); - Program.ScheduleRenewal(TotalTarget); + Log.Information("Adding renewal for {binding}", totalTarget); + Program.ScheduleRenewal(totalTarget); } } } diff --git a/letsencrypt-win-simple/Plugin/ManualPlugin.cs b/letsencrypt-win-simple/Plugin/ManualPlugin.cs index b6b9f6e7c..d0ca16bcf 100644 --- a/letsencrypt-win-simple/Plugin/ManualPlugin.cs +++ b/letsencrypt-win-simple/Plugin/ManualPlugin.cs @@ -64,11 +64,12 @@ public override void HandleMenuResponse(string response, List targets) var hostName = Console.ReadLine(); string[] alternativeNames = null; - if(Program.Options.SAN) + if(Program.Options.San) { Console.Write("Enter all Alternative Names seperated by a comma "); - var SANInput = Console.ReadLine(); - alternativeNames = SANInput.Split(','); + Console.SetIn(new System.IO.StreamReader(Console.OpenStandardInput(8192))); + var sanInput = Console.ReadLine(); + alternativeNames = sanInput.Split(','); } @@ -79,17 +80,17 @@ public override void HandleMenuResponse(string response, List targets) // TODO: make a system where they can execute a program/batch file to update whatever they need after install. - List SANList = new List(alternativeNames); - if (SANList.Count <= 100) + List sanList = new List(alternativeNames); + if (sanList.Count <= 100) { - var target = new Target() { Host = hostName, WebRootPath = physicalPath, PluginName = Name, AlternativeNames = SANList }; + var target = new Target() { Host = hostName, WebRootPath = physicalPath, PluginName = Name, AlternativeNames = sanList }; Program.Auto(target); } else { Console.WriteLine($" You entered too many hosts for a SAN certificate. Let's Encrypt currently has a maximum of 100 alternative names per certificate."); - Log.Error("You entered too many hosts for a SAN certificate. Let's Encrypt currently has a maximum of 100 alternative names per certificate."); + Log.Error("You entered too many hosts for a San certificate. Let's Encrypt currently has a maximum of 100 alternative names per certificate."); } } } diff --git a/letsencrypt-win-simple/Plugin/Plugin.cs b/letsencrypt-win-simple/Plugin/Plugin.cs index 781bac38b..72588dfa9 100644 --- a/letsencrypt-win-simple/Plugin/Plugin.cs +++ b/letsencrypt-win-simple/Plugin/Plugin.cs @@ -24,7 +24,7 @@ public abstract class Plugin public abstract List GetTargets(); /// - /// Generates a list of sites that SAN certificates can be created for. + /// Generates a list of sites that San certificates can be created for. /// /// public abstract List GetSites(); diff --git a/letsencrypt-win-simple/Program.cs b/letsencrypt-win-simple/Program.cs index 6ce0ca24d..a568d90b4 100644 --- a/letsencrypt-win-simple/Program.cs +++ b/letsencrypt-win-simple/Program.cs @@ -1,4 +1,5 @@ using System; +using System.CodeDom; using System.Collections.Generic; using System.IO; using System.Linq; @@ -21,18 +22,20 @@ namespace LetsEncrypt.ACME.Simple { class Program { - const string clientName = "letsencrypt-win-simple"; - public static string BaseURI { get; set; } - static string configPath; - static string certificatePath; - static Settings settings; - static AcmeClient client; + private const string ClientName = "letsencrypt-win-simple"; + private static string _certificateStore = "WebHosting"; + public static float RenewalPeriod = 60; + public static bool CentralSsl = false; + public static string BaseUri { get; set; } + private static string _configPath; + private static string _certificatePath; + private static Settings _settings; + private static AcmeClient _client; public static Options Options; - public static bool CentralSSL = false; static bool IsElevated => new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); - static void Main(string[] args) + private static void Main(string[] args) { Log.Logger = new LoggerConfiguration() .ReadFrom.AppSettings() @@ -55,57 +58,78 @@ static void Main(string[] args) Options = parsed.Value; Log.Debug("{@Options}", Options); Console.WriteLine("Let's Encrypt (Simple Windows ACME Client)"); - BaseURI = Options.BaseURI; + BaseUri = Options.BaseUri; if (Options.Test) { - BaseURI = "https://acme-staging.api.letsencrypt.org/"; - Log.Debug("Test paramater set: {BaseURI}", BaseURI); + BaseUri = "https://acme-staging.api.letsencrypt.org/"; + Log.Debug("Test paramater set: {BaseUri}", BaseUri); } - if (Options.SAN) + if (Options.San) { - Log.Debug("SAN Option Enabled: Running per site and not per host"); + Log.Debug("San Option Enabled: Running per site and not per host"); } - Console.WriteLine($"\nACME Server: {BaseURI}"); - Log.Information("ACME Server: {BaseURI}", BaseURI); + try + { + RenewalPeriod = Properties.Settings.Default.RenewalDays; + Console.WriteLine("Renewal Period: " + RenewalPeriod); + Log.Information("Renewal Period: {RenewalPeriod}", RenewalPeriod); + } + catch (Exception ex) + { + Log.Warning("Error reading RenewalDays from app config, defaulting to {RenewalPeriod} Error: {@ex}", RenewalPeriod.ToString(), ex); + } + try + { + _certificateStore = Properties.Settings.Default.CertificateStore; + Console.WriteLine("Certificate Store: " + _certificateStore); + Log.Information("Certificate Store: {_certificateStore}", _certificateStore); + } + catch (Exception ex) + { + Log.Warning("Error reading CertificateStore from app config, defaulting to {_certificateStore} Error: {@ex}", _certificateStore, ex); + } - if (!string.IsNullOrWhiteSpace(Options.CentralSSLStore)) + Console.WriteLine($"\nACME Server: {BaseUri}"); + Log.Information("ACME Server: {BaseUri}", BaseUri); + + if (!string.IsNullOrWhiteSpace(Options.CentralSslStore)) { - Console.WriteLine("Using Centralized SSL Path: " + Options.CentralSSLStore); - Log.Information("Using Centralized SSL Path: {CentralSSLStore}", Options.CentralSSLStore); - CentralSSL = true; + Console.WriteLine("Using Centralized SSL Path: " + Options.CentralSslStore); + Log.Information("Using Centralized SSL Path: {CentralSslStore}", Options.CentralSslStore); + CentralSsl = true; } - settings = new Settings(clientName, BaseURI); - Log.Debug("{@settings}", settings); - configPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), clientName, CleanFileName(BaseURI)); - Console.WriteLine("Config Folder: " + configPath); - Log.Information("Config Folder: {configPath}", configPath); - Directory.CreateDirectory(configPath); + _settings = new Settings(ClientName, BaseUri); + Log.Debug("{@_settings}", _settings); + _configPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ClientName, CleanFileName(BaseUri)); + Console.WriteLine("Config Folder: " + _configPath); + Log.Information("Config Folder: {_configPath}", _configPath); + Directory.CreateDirectory(_configPath); - certificatePath = Properties.Settings.Default.CertificatePath; + _certificatePath = Properties.Settings.Default.CertificatePath; - if (string.IsNullOrWhiteSpace(certificatePath)) + if (string.IsNullOrWhiteSpace(_certificatePath)) { - certificatePath = configPath; + _certificatePath = _configPath; } else { try { - Directory.CreateDirectory(certificatePath); + Directory.CreateDirectory(_certificatePath); } catch (Exception ex) { - Console.WriteLine($"Error creating the certificate directory, {certificatePath}. Defaulting to config path"); - Log.Warning("Error creating the certificate directory, {certificatePath}. Defaulting to config path. Error: {@ex}", certificatePath, ex); + Console.WriteLine($"Error creating the certificate directory, {_certificatePath}. Defaulting to config path"); + Log.Warning("Error creating the certificate directory, {_certificatePath}. Defaulting to config path. Error: {@ex}", _certificatePath, ex); - certificatePath = configPath; + _certificatePath = _configPath; } } - Console.WriteLine("Certificate Folder: " + certificatePath); - Log.Information("Certificate Folder: {certificatePath}", certificatePath); + Console.WriteLine("Certificate Folder: " + _certificatePath); + Log.Information("Certificate Folder: {_certificatePath}", _certificatePath); try { @@ -113,7 +137,7 @@ static void Main(string[] args) { signer.Init(); - var signerPath = Path.Combine(configPath, "Signer"); + var signerPath = Path.Combine(_configPath, "Signer"); if (File.Exists(signerPath)) { Console.WriteLine($"Loading Signer from {signerPath}"); @@ -122,20 +146,20 @@ static void Main(string[] args) signer.Load(signerStream); } - using (client = new AcmeClient(new Uri(BaseURI), new AcmeServerDirectory(), signer)) + using (_client = new AcmeClient(new Uri(BaseUri), new AcmeServerDirectory(), signer)) { - client.Init(); + _client.Init(); Console.WriteLine("\nGetting AcmeServerDirectory"); Log.Information("Getting AcmeServerDirectory"); - client.GetDirectory(true); + _client.GetDirectory(true); - var registrationPath = Path.Combine(configPath, "Registration"); + var registrationPath = Path.Combine(_configPath, "Registration"); if (File.Exists(registrationPath)) { Console.WriteLine($"Loading Registration from {registrationPath}"); Log.Information("Loading Registration from {registrationPath}", registrationPath); using (var registrationStream = File.OpenRead(registrationPath)) - client.Registration = AcmeRegistration.Load(registrationStream); + _client.Registration = AcmeRegistration.Load(registrationStream); } else { @@ -152,9 +176,9 @@ static void Main(string[] args) Console.WriteLine("Calling Register"); Log.Information("Calling Register"); - var registration = client.Register(contacts); + var registration = _client.Register(contacts); - if (!Options.AcceptTOS && !Options.Renew) + if (!Options.AcceptTos && !Options.Renew) { Console.WriteLine($"Do you agree to {registration.TosLinkUri}? (Y/N) "); if (!PromptYesNo()) @@ -163,12 +187,12 @@ static void Main(string[] args) Console.WriteLine("Updating Registration"); Log.Information("Updating Registration"); - client.UpdateRegistration(true, true); + _client.UpdateRegistration(true, true); Console.WriteLine("Saving Registration"); Log.Information("Saving Registration"); using (var registrationStream = File.OpenWrite(registrationPath)) - client.Registration.Save(registrationStream); + _client.Registration.Save(registrationStream); Console.WriteLine("Saving Signer"); Log.Information("Saving Signer"); @@ -186,7 +210,7 @@ static void Main(string[] args) return; } var targets = new List(); - if (!Options.SAN) + if (!Options.San) { foreach (var plugin in Target.Plugins.Values) { @@ -209,26 +233,26 @@ static void Main(string[] args) } else { - int HostsPerPage = 50; + int hostsPerPage = 50; try { - HostsPerPage = Properties.Settings.Default.HostsPerPage; + hostsPerPage = Properties.Settings.Default.HostsPerPage; } catch (Exception ex) { Log.Error("Error getting HostsPerPage setting, setting to default value. Error: {@ex}", ex); } var count = 1; - if (targets.Count > HostsPerPage) + if (targets.Count > hostsPerPage) { do { - if ((count + HostsPerPage) <= targets.Count) + if ((count + hostsPerPage) <= targets.Count) { - int stop = count + HostsPerPage; + int stop = count + hostsPerPage; for (int i = count; i < stop; i++) { - if (!Options.SAN) + if (!Options.San) { Console.WriteLine($" {count}: {targets[count - 1]}"); } @@ -243,7 +267,7 @@ static void Main(string[] args) { for (int i = count; i <= targets.Count; i++) { - if (!Options.SAN) + if (!Options.San) { Console.WriteLine($" {count}: {targets[count - 1]}"); } @@ -347,7 +371,7 @@ static void Main(string[] args) Console.ReadLine(); } - static string CleanFileName(string fileName) => Path.GetInvalidFileNameChars().Aggregate(fileName, (current, c) => current.Replace(c.ToString(), string.Empty)); + private static string CleanFileName(string fileName) => Path.GetInvalidFileNameChars().Aggregate(fileName, (current, c) => current.Replace(c.ToString(), string.Empty)); public static bool PromptYesNo() { @@ -376,10 +400,10 @@ public static void Auto(Target binding) return; } - X509Store store; - X509Certificate2 certificate; - if (!CentralSSL) + if (!CentralSsl) { + X509Store store; + X509Certificate2 certificate; Log.Information("Installing Non-Central SSL Certificate in the certificate store"); InstallCertificate(binding, pfxFilename, out store, out certificate); if (Options.Test && !Options.Renew) @@ -401,7 +425,7 @@ public static void Auto(Target binding) if (Options.Test && !Options.Renew) { - Console.WriteLine($"\nDo you want to automatically renew this certificate in 60 days? This will add a task scheduler task. (Y/N) "); + Console.WriteLine($"\nDo you want to automatically renew this certificate in {RenewalPeriod} days? This will add a task scheduler task. (Y/N) "); if (!PromptYesNo()) return; } @@ -418,7 +442,7 @@ public static void InstallCertificate(Target binding, string pfxFilename, out X5 { try { - store = new X509Store("WebHosting", StoreLocation.LocalMachine); + store = new X509Store(_certificateStore, StoreLocation.LocalMachine); store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite); } catch (CryptographicException) @@ -426,6 +450,11 @@ public static void InstallCertificate(Target binding, string pfxFilename, out X5 store = new X509Store(StoreName.My, StoreLocation.LocalMachine); store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite); } + catch (Exception ex) + { + Log.Error("Error encountered while opening certificate store. Error: {@ex}", ex); + throw new Exception(ex.Message); + } Console.WriteLine($" Opened Certificate Store \"{store.Name}\""); Log.Information("Opened Certificate Store {Name}", store.Name); @@ -460,10 +489,10 @@ public static string GetCertificate(Target binding) { var dnsIdentifier = binding.Host; - var SANList = binding.AlternativeNames; + var sanList = binding.AlternativeNames; List allDnsIdentifiers = new List(); - if (!Options.SAN) + if (!Options.San) { allDnsIdentifiers.Add(binding.Host); } @@ -499,11 +528,11 @@ public static string GetCertificate(Target binding) { CommonName = allDnsIdentifiers[0], }; - if(SANList != null) + if(sanList != null) { - if (SANList.Count > 0) + if (sanList.Count > 0) { - csrDetails.AlternativeNames = SANList; + csrDetails.AlternativeNames = sanList; } } var csrParams = new CsrParams @@ -518,11 +547,11 @@ public static string GetCertificate(Target binding) cp.ExportCsr(csr, EncodingFormat.DER, bs); derRaw = bs.ToArray(); } - var derB64u = JwsHelper.Base64UrlEncode(derRaw); + var derB64U = JwsHelper.Base64UrlEncode(derRaw); Console.WriteLine($"\nRequesting Certificate"); Log.Information("Requesting Certificate"); - var certRequ = client.RequestCertificate(derB64u); + var certRequ = _client.RequestCertificate(derB64U); Log.Debug("certRequ {@certRequ}", certRequ); @@ -531,20 +560,20 @@ public static string GetCertificate(Target binding) if (certRequ.StatusCode == System.Net.HttpStatusCode.Created) { - var keyGenFile = Path.Combine(certificatePath, $"{dnsIdentifier}-gen-key.json"); - var keyPemFile = Path.Combine(certificatePath, $"{dnsIdentifier}-key.pem"); - var csrGenFile = Path.Combine(certificatePath, $"{dnsIdentifier}-gen-csr.json"); - var csrPemFile = Path.Combine(certificatePath, $"{dnsIdentifier}-csr.pem"); - var crtDerFile = Path.Combine(certificatePath, $"{dnsIdentifier}-crt.der"); - var crtPemFile = Path.Combine(certificatePath, $"{dnsIdentifier}-crt.pem"); + var keyGenFile = Path.Combine(_certificatePath, $"{dnsIdentifier}-gen-key.json"); + var keyPemFile = Path.Combine(_certificatePath, $"{dnsIdentifier}-key.pem"); + var csrGenFile = Path.Combine(_certificatePath, $"{dnsIdentifier}-gen-csr.json"); + var csrPemFile = Path.Combine(_certificatePath, $"{dnsIdentifier}-csr.pem"); + var crtDerFile = Path.Combine(_certificatePath, $"{dnsIdentifier}-crt.der"); + var crtPemFile = Path.Combine(_certificatePath, $"{dnsIdentifier}-crt.pem"); string crtPfxFile = null; - if (!CentralSSL) + if (!CentralSsl) { - crtPfxFile = Path.Combine(certificatePath, $"{dnsIdentifier}-all.pfx"); + crtPfxFile = Path.Combine(_certificatePath, $"{dnsIdentifier}-all.pfx"); } else { - crtPfxFile = Path.Combine(Options.CentralSSLStore, $"{dnsIdentifier}.pfx"); + crtPfxFile = Path.Combine(Options.CentralSslStore, $"{dnsIdentifier}.pfx"); } using (var fs = new FileStream(keyGenFile, FileMode.Create)) @@ -572,14 +601,14 @@ public static string GetCertificate(Target binding) // To generate a PKCS#12 (.PFX) file, we need the issuer's public certificate var isuPemFile = GetIssuerCertificate(certRequ, cp); - Log.Debug("CentralSSL {CentralSSL} SAN {SAN}", CentralSSL.ToString(), Options.SAN.ToString()); + Log.Debug("CentralSsl {CentralSsl} San {San}", CentralSsl.ToString(), Options.San.ToString()); - if(CentralSSL && Options.SAN) + if(CentralSsl && Options.San) { foreach (var host in allDnsIdentifiers) { Console.WriteLine($"Host: {host}"); - crtPfxFile = Path.Combine(Options.CentralSSLStore, $"{host}.pfx"); + crtPfxFile = Path.Combine(Options.CentralSslStore, $"{host}.pfx"); Console.WriteLine($" Saving Certificate to {crtPfxFile}"); Log.Information("Saving Certificate to {crtPfxFile}", crtPfxFile); @@ -601,7 +630,7 @@ public static string GetCertificate(Target binding) } } } - else //Central SSL and SAN need to save the cert for each hostname + else //Central SSL and San need to save the cert for each hostname { Console.WriteLine($" Saving Certificate to {crtPfxFile}"); Log.Information("Saving Certificate to {crtPfxFile}", crtPfxFile); @@ -633,12 +662,12 @@ public static string GetCertificate(Target binding) public static void EnsureTaskScheduler() { - var taskName = $"{clientName} {CleanFileName(BaseURI)}"; + var taskName = $"{ClientName} {CleanFileName(BaseUri)}"; using (var taskService = new TaskService()) { bool addTask = true; - if (settings.ScheduledTaskName == taskName) + if (_settings.ScheduledTaskName == taskName) { addTask = false; Console.WriteLine($"\nDo you want to replace the existing {taskName} task? (Y/N) "); @@ -666,7 +695,7 @@ public static void EnsureTaskScheduler() var currentExec = Assembly.GetExecutingAssembly().Location; // Create an action that will launch the app with the renew paramaters whenever the trigger fires - task.Actions.Add(new ExecAction(currentExec, $"--renew --baseuri \"{BaseURI}\"", Path.GetDirectoryName(currentExec))); + task.Actions.Add(new ExecAction(currentExec, $"--renew --baseuri \"{BaseUri}\"", Path.GetDirectoryName(currentExec))); task.Principal.RunLevel = TaskRunLevel.Highest; // need admin Log.Debug("{@task}", task); @@ -674,18 +703,18 @@ public static void EnsureTaskScheduler() // Register the task in the root folder taskService.RootFolder.RegisterTaskDefinition(taskName, task); - settings.ScheduledTaskName = taskName; + _settings.ScheduledTaskName = taskName; } } } - const float renewalPeriod = 60; // can't easily make this a command line option since it would have to be saved + public static void ScheduleRenewal(Target target) { EnsureTaskScheduler(); - var renewals = settings.LoadRenewals(); + var renewals = _settings.LoadRenewals(); foreach (var existing in from r in renewals.ToArray() where r.Binding.Host == target.Host select r) { @@ -694,9 +723,9 @@ public static void ScheduleRenewal(Target target) renewals.Remove(existing); } - var result = new ScheduledRenewal() { Binding = target, CentralSSL = Options.CentralSSLStore, SAN = Options.SAN.ToString(), Date = DateTime.UtcNow.AddDays(renewalPeriod) }; + var result = new ScheduledRenewal() { Binding = target, CentralSsl = Options.CentralSslStore, San = Options.San.ToString(), Date = DateTime.UtcNow.AddDays(RenewalPeriod) }; renewals.Add(result); - settings.SaveRenewals(renewals); + _settings.SaveRenewals(renewals); Console.WriteLine($" Renewal Scheduled {result}"); Log.Information("Renewal Scheduled {result}", result); @@ -707,7 +736,7 @@ public static void CheckRenewals() Console.WriteLine("Checking Renewals"); Log.Information("Checking Renewals"); - var renewals = settings.LoadRenewals(); + var renewals = _settings.LoadRenewals(); if (renewals.Count == 0) { Console.WriteLine(" No scheduled renewals found."); @@ -723,32 +752,32 @@ public static void CheckRenewals() { Console.WriteLine($" Renewing certificate for {renewal}"); Log.Information("Renewing certificate for {renewal}", renewal); - if (string.IsNullOrWhiteSpace(renewal.CentralSSL)) + if (string.IsNullOrWhiteSpace(renewal.CentralSsl)) { //Not using Central SSL - CentralSSL = false; - Options.CentralSSLStore = null; + CentralSsl = false; + Options.CentralSslStore = null; } else { //Using Central SSL - CentralSSL = true; - Options.CentralSSLStore = renewal.CentralSSL; + CentralSsl = true; + Options.CentralSslStore = renewal.CentralSsl; } - if(string.IsNullOrWhiteSpace(renewal.SAN)) + if(string.IsNullOrWhiteSpace(renewal.San)) { - //Not using SAN - Options.SAN = false; + //Not using San + Options.San = false; } - else if(renewal.SAN.ToLower() == "true") + else if(renewal.San.ToLower() == "true") { - //Using SAN - Options.SAN = true; + //Using San + Options.San = true; } else { - //Not using SAN - Options.SAN = false; + //Not using San + Options.San = false; } if (renewal.Binding.PluginName == "IIS") { @@ -759,8 +788,8 @@ public static void CheckRenewals() renewal.Binding.Plugin.Renew(renewal.Binding); } - renewal.Date = DateTime.UtcNow.AddDays(renewalPeriod); - settings.SaveRenewals(renewals); + renewal.Date = DateTime.UtcNow.AddDays(RenewalPeriod); + _settings.SaveRenewals(renewals); Console.WriteLine($" Renewal Scheduled {renewal}"); Log.Information("Renewal Scheduled {renewal}", renewal); } @@ -782,7 +811,7 @@ public static string GetIssuerCertificate(CertificateRequest certificate, Certif using (var web = new WebClient()) { - var uri = new Uri(new Uri(BaseURI), upLink.Uri); + var uri = new Uri(new Uri(BaseUri), upLink.Uri); web.DownloadFile(uri, tmp); } @@ -792,8 +821,8 @@ public static string GetIssuerCertificate(CertificateRequest certificate, Certif var sigalg = cacert.SignatureAlgorithm?.FriendlyName; var sigval = cacert.GetCertHashString(); - var cacertDerFile = Path.Combine(certificatePath, $"ca-{sernum}-crt.der"); - var cacertPemFile = Path.Combine(certificatePath, $"ca-{sernum}-crt.pem"); + var cacertDerFile = Path.Combine(_certificatePath, $"ca-{sernum}-crt.der"); + var cacertPemFile = Path.Combine(_certificatePath, $"ca-{sernum}-crt.pem"); if (!File.Exists(cacertDerFile)) File.Copy(tmp, cacertDerFile, true); @@ -824,7 +853,7 @@ public static string GetIssuerCertificate(CertificateRequest certificate, Certif public static AuthorizationState Authorize(Target target) { List dnsIdentifiers = new List(); - if (!Options.SAN) + if (!Options.San) { dnsIdentifiers.Add(target.Host); } @@ -841,8 +870,8 @@ public static AuthorizationState Authorize(Target target) Console.WriteLine($"\nAuthorizing Identifier {dnsIdentifier} Using Challenge Type {AcmeProtocol.CHALLENGE_TYPE_HTTP}"); Log.Information("Authorizing Identifier {dnsIdentifier} Using Challenge Type {CHALLENGE_TYPE_HTTP}", dnsIdentifier, AcmeProtocol.CHALLENGE_TYPE_HTTP); - var authzState = client.AuthorizeIdentifier(dnsIdentifier); - var challenge = client.DecodeChallenge(authzState, AcmeProtocol.CHALLENGE_TYPE_HTTP); + var authzState = _client.AuthorizeIdentifier(dnsIdentifier); + var challenge = _client.DecodeChallenge(authzState, AcmeProtocol.CHALLENGE_TYPE_HTTP); var httpChallenge = challenge.Challenge as HttpChallenge; // We need to strip off any leading '/' in the path @@ -868,7 +897,7 @@ public static AuthorizationState Authorize(Target target) Console.WriteLine(" Submitting answer"); Log.Information("Submitting answer"); authzState.Challenges = new AuthorizeChallenge[] { challenge }; - client.SubmitChallengeAnswer(authzState, AcmeProtocol.CHALLENGE_TYPE_HTTP, true); + _client.SubmitChallengeAnswer(authzState, AcmeProtocol.CHALLENGE_TYPE_HTTP, true); // have to loop to wait for server to stop being pending. // TODO: put timeout/retry limit in this loop @@ -877,7 +906,7 @@ public static AuthorizationState Authorize(Target target) Console.WriteLine(" Refreshing authorization"); Log.Information("Refreshing authorization"); Thread.Sleep(4000); // this has to be here to give ACME server a chance to think - var newAuthzState = client.RefreshIdentifierAuthorization(authzState); + var newAuthzState = _client.RefreshIdentifierAuthorization(authzState); if (newAuthzState.Status != "pending") authzState = newAuthzState; } diff --git a/letsencrypt-win-simple/Properties/AssemblyInfo.cs b/letsencrypt-win-simple/Properties/AssemblyInfo.cs index a08e12998..5a4bac2a1 100644 --- a/letsencrypt-win-simple/Properties/AssemblyInfo.cs +++ b/letsencrypt-win-simple/Properties/AssemblyInfo.cs @@ -33,4 +33,4 @@ // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.8.7.*")] -[assembly: AssemblyFileVersion("1.8.7.0")] +[assembly: AssemblyFileVersion("1.8.7.5")] diff --git a/letsencrypt-win-simple/Properties/Settings.Designer.cs b/letsencrypt-win-simple/Properties/Settings.Designer.cs index d6ba0f1c6..1d0e29a30 100644 --- a/letsencrypt-win-simple/Properties/Settings.Designer.cs +++ b/letsencrypt-win-simple/Properties/Settings.Designer.cs @@ -67,5 +67,23 @@ public string CertificatePath { return ((string)(this["CertificatePath"])); } } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("60")] + public float RenewalDays { + get { + return ((float)(this["RenewalDays"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("WebHosting")] + public string CertificateStore { + get { + return ((string)(this["CertificateStore"])); + } + } } } diff --git a/letsencrypt-win-simple/Properties/Settings.settings b/letsencrypt-win-simple/Properties/Settings.settings index e290424ca..9d9dd857e 100644 --- a/letsencrypt-win-simple/Properties/Settings.settings +++ b/letsencrypt-win-simple/Properties/Settings.settings @@ -17,5 +17,11 @@ + + 60 + + + WebHosting + \ No newline at end of file diff --git a/letsencrypt-win-simple/ScheduledRenewal.cs b/letsencrypt-win-simple/ScheduledRenewal.cs index 5daa3252b..66b41ad30 100644 --- a/letsencrypt-win-simple/ScheduledRenewal.cs +++ b/letsencrypt-win-simple/ScheduledRenewal.cs @@ -12,8 +12,8 @@ public class ScheduledRenewal { public DateTime Date { get; set; } public Target Binding { get; set; } - public string CentralSSL { get; set; } - public string SAN { get; set; } + public string CentralSsl { get; set; } + public string San { get; set; } public override string ToString() => $"{Binding} Renew After {Date.ToShortDateString()}"; diff --git a/letsencrypt-win-simple/letsencrypt-win-simple.csproj b/letsencrypt-win-simple/letsencrypt-win-simple.csproj index 05e4844a7..c9e946f74 100644 --- a/letsencrypt-win-simple/letsencrypt-win-simple.csproj +++ b/letsencrypt-win-simple/letsencrypt-win-simple.csproj @@ -130,6 +130,7 @@ + From af2eebc1fbcd8f01c3ab11e6658575a6a5e774c5 Mon Sep 17 00:00:00 2001 From: rkerber Date: Mon, 8 Feb 2016 15:36:48 -0600 Subject: [PATCH 4/4] Updating Version --- letsencrypt-win-simple/Properties/AssemblyInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-win-simple/Properties/AssemblyInfo.cs b/letsencrypt-win-simple/Properties/AssemblyInfo.cs index 5a4bac2a1..7f1c60564 100644 --- a/letsencrypt-win-simple/Properties/AssemblyInfo.cs +++ b/letsencrypt-win-simple/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.8.7.*")] -[assembly: AssemblyFileVersion("1.8.7.5")] +[assembly: AssemblyVersion("1.8.8.*")] +[assembly: AssemblyFileVersion("1.8.8.1")]