public void Install(PrivateKey pk, Crt crt, IEnumerable <Crt> chain, IPkiTool cp) { var bindings = IisHelper.ResolveSiteBindings(WebSiteRef); var existing = IisHelper.ResolveSiteBindings( BindingAddress, BindingPort, BindingHost, bindings).ToArray(); if (existing?.Length > 0 && !Force) { throw new InvalidOperationException( "found existing conflicting bindings for target site;" + " use Force parameter to overwrite"); } // TODO: should we expose these as optional params to be overridden by user? var storeLocation = StoreLocation.LocalMachine; var storeName = StoreName.My; var cert = ImportCertificate(pk, crt, chain, cp, storeName, storeLocation, CertificateFriendlyName); var certStore = Enum.GetName(typeof(StoreName), storeName); var certHash = cert.GetCertHash(); if (existing?.Length > 0) { foreach (var oldBinding in existing) { if (BindingHostRequired.HasValue) { oldBinding.BindingHostRequired = BindingHostRequired; } IisHelper.UpdateSiteBinding(oldBinding, certStore, certHash); } } else { var firstBinding = bindings.First(); var newBinding = new IisWebSiteBinding { // Copy over some existing site info SiteId = firstBinding.SiteId, SiteName = firstBinding.SiteName, SiteRoot = firstBinding.SiteRoot, // New binding specifics BindingProtocol = "https", BindingAddress = this.BindingAddress, BindingPort = this.BindingPort.ToString(), BindingHost = this.BindingHost, BindingHostRequired = this.BindingHostRequired, }; IisHelper.CreateSiteBinding(newBinding, certStore, certHash); } }
public static void CreateSiteBinding(IisWebSiteBinding binding, string certStore, byte[] certHash) { using (var iis = new ServerManager()) { var sites = iis.Sites.Where(_ => _.Id == binding.SiteId).ToArray(); if (sites?.Length == 0) { throw new ArgumentException("no matching sites found") .With(nameof(binding.SiteId), binding.SiteId); } foreach (var site in sites) { // Binding Information spec // (https://msdn.microsoft.com/en-us/library/bb339271(v=vs.90).aspx): // // IpAddr:Port:HostHeader // // where IpAddr can be * for all interfaces // where HostHeader is only valid for SNI-capable IIS (8+) var bindingAddr = string.IsNullOrEmpty(binding.BindingAddress) ? "*" : binding.BindingAddress; var bindingPort = string.IsNullOrEmpty(binding.BindingPort) ? "443" : int.Parse(binding.BindingPort).ToString(); var bindingHost = string.IsNullOrEmpty(binding.BindingHost) ? "" : binding.BindingHost; var bindingInfo = $"{bindingAddr}:{bindingPort}:{bindingHost}"; var b = site.Bindings.Add(bindingInfo, certHash, certStore); // PATCH - Set SNI flag for new binding if (binding.BindingHostRequired.GetValueOrDefault() && GetIisVersion().Major >= 8) { b.SetAttributeValue("sslFlags", 1); } else { b.SetAttributeValue("sslFlags", 3); } } iis.CommitChanges(); } }
public static void UpdateSiteBinding(IisWebSiteBinding binding, string certStore, byte[] certHash) { using (var iis = new ServerManager()) { var sites = iis.Sites.Where(_ => _.Id == binding.SiteId).ToArray(); if (sites?.Length == 0) { throw new ArgumentException("no matching sites found") .With(nameof(binding.SiteId), binding.SiteId); } var bindingCount = 0; foreach (var site in sites) { foreach (var b in site.Bindings) { if (!string.Equals(binding.BindingProtocol, b.Protocol)) { continue; } if (!string.Equals(binding.BindingAddress, b.EndPoint?.Address?.ToString())) { continue; } if (!string.Equals(binding.BindingHost, b.Host)) { continue; } if (!string.Equals(binding.BindingPort, b.EndPoint?.Port.ToString())) { continue; } ++bindingCount; b.CertificateStoreName = certStore; b.CertificateHash = certHash; if (binding.BindingHostRequired.GetValueOrDefault() && GetIisVersion().Major >= 8) { b.SetAttributeValue("sslFlags", 1); } else { b.SetAttributeValue("sslFlags", 3); } } } if (bindingCount == 0) { throw new ArgumentException("no matching bindings found") .With(nameof(binding.SiteId), binding.SiteId) .With(nameof(binding.BindingProtocol), binding.BindingProtocol) .With(nameof(binding.BindingAddress), binding.BindingAddress) .With(nameof(binding.BindingPort), binding.BindingPort) .With(nameof(binding.BindingHost), binding.BindingHost); } iis.CommitChanges(); } }
private void EditFile(HttpChallenge httpChallenge, bool delete, TextWriter msg) { IisWebSiteBinding site = IisHelper.ResolveSingleSite(WebSiteRef, IisHelper.ListDistinctHttpWebSites()); var siteRoot = site.SiteRoot; if (!string.IsNullOrEmpty(OverrideSiteRoot)) { siteRoot = OverrideSiteRoot; } if (string.IsNullOrEmpty(siteRoot)) { throw new InvalidOperationException("missing root path for resolve site") .With(nameof(IisWebSiteBinding.SiteId), site.SiteId) .With(nameof(IisWebSiteBinding.SiteName), site.SiteName); } // IIS-configured Site Root can use env vars siteRoot = Environment.ExpandEnvironmentVariables(siteRoot); // Make sure we're using the canonical full path siteRoot = Path.GetFullPath(siteRoot); // We need to strip off any leading '/' in the path var filePath = httpChallenge.FilePath; if (filePath.StartsWith("/")) { filePath = filePath.Substring(1); } var fullFilePath = Path.Combine(siteRoot, filePath); var fullDirPath = Path.GetDirectoryName(fullFilePath); var fullConfigPath = Path.Combine(fullDirPath, "web.config"); // This meta-data file will be placed next to the actual // Challenge answer content file and it captures some details // that we need in order to properly clean up the handling of // this Challenge after it has been submitted var fullMetaPath = $"{fullFilePath}-acmesharp_meta"; // Check if user is running with elevated privs and warn if not if (!IisHelper.IsAdministrator()) { Console.Error.WriteLine("WARNING: You are not running with elelvated privileges."); Console.Error.WriteLine(" Write access may be denied to the destination."); } if (delete) { bool skipLocalWebConfig = SkipLocalWebConfig; List <string> dirsCreated = null; // First see if there's a meta file there to help us out if (File.Exists(fullMetaPath)) { var meta = JsonHelper.Load <IisChallengeHandlerMeta>( File.ReadAllText(fullMetaPath)); skipLocalWebConfig = meta.SkippedLocalWebConfig; dirsCreated = meta.DirsCreated; } // Get rid of the Challenge answer content file if (File.Exists(fullFilePath)) { File.Delete(fullFilePath); msg.WriteLine("* Challenge response content has been removed from local file path"); msg.WriteLine(" at: [{0}]", fullFilePath); } // Get rid of web.config if necessary if (!skipLocalWebConfig && File.Exists(fullConfigPath)) { File.Delete(fullConfigPath); msg.WriteLine("* Local web.config has been removed from local file path"); msg.WriteLine(" at: [{0}]", fullFilePath); } // Get rid of the meta file so that we can clean up the dirs if (File.Exists(fullMetaPath)) { File.Delete(fullMetaPath); } // Walk up the tree if needed if (dirsCreated?.Count > 0) { var dirsDeleted = new List <string>(); dirsCreated.Reverse(); foreach (var dir in dirsCreated) { if (Directory.Exists(dir)) { if (Directory.GetFileSystemEntries(dir).Length == 0) { Directory.Delete(dir); dirsDeleted.Add(dir); } } } if (dirsDeleted.Count > 0) { msg.WriteLine("* Removed the following directories:"); foreach (var dd in dirsDeleted) { msg.WriteLine(" - [{0}]", dd); } } } } else { // Figure out which dirs we have to create so // we can capture and clean it up later on var meta = new IisChallengeHandlerMeta { WasAdmin = IisHelper.IsAdministrator(), SkippedLocalWebConfig = SkipLocalWebConfig, DirsCreated = new List <string>(), }; // In theory this ascending of the dir path should work // just fine, but just in case, this path segment counter // should gaurd against the possibility of an infinite loop var dirLimit = 100; var testDir = fullDirPath; while (!Directory.Exists(testDir)) { // Sanity check against an infinite loop if (--dirLimit <= 0) { throw new Exception("Unexpected directory path segment count reached") .With(nameof(dirLimit), "100") .With(nameof(fullDirPath), fullDirPath) .With(nameof(testDir), testDir) .With($"first-{nameof(meta.DirsCreated)}", meta.DirsCreated[0]) .With($"last-{nameof(meta.DirsCreated)}", meta.DirsCreated[meta.DirsCreated.Count - 1]); } if (Path.GetFullPath(testDir) == siteRoot) { break; } // Add to the top of the list meta.DirsCreated.Insert(0, testDir); // Move to the parent testDir = Path.GetDirectoryName(testDir); } foreach (var dir in meta.DirsCreated) { Directory.CreateDirectory(dir); } File.WriteAllText(fullFilePath, httpChallenge.FileContent); File.WriteAllText(fullMetaPath, JsonHelper.Save(meta)); msg.WriteLine("* Challenge response content has been written to local file path"); msg.WriteLine(" at: [{0}]", fullFilePath); msg.WriteLine("* Challenge response should be accessible with a MIME type of [text/json]"); msg.WriteLine(" at: [{0}]", httpChallenge.FileUrl); if (!SkipLocalWebConfig) { var t = typeof(IisChallengeHandler); var r = $"{t.Namespace}.{t.Name}-WebConfig"; using (Stream rs = t.Assembly.GetManifestResourceStream(r)) { using (var fs = new FileStream(fullConfigPath, FileMode.Create)) { rs.CopyTo(fs); } } msg.WriteLine("* Local web.config has been created to serve response file as JSON"); msg.WriteLine(" however, you may need to adjust this file for your environment"); msg.WriteLine(" at: [{0}]", httpChallenge.FileUrl); } else { msg.WriteLine("* Local web.config file creation has been skipped!"); msg.WriteLine(" You may need to manually adjust your configuration to serve the"); msg.WriteLine(" Challenge Response file with a MIME type of [text/json]"); } } }