/// <summary> /// Update/create bindings for all host names in the certificate /// </summary> /// <param name="target"></param> /// <param name="flags"></param> /// <param name="thumbprint"></param> /// <param name="store"></param> public int AddOrUpdateBindings( IEnumerable <Identifier> identifiers, BindingOptions bindingOptions, byte[]?oldThumbprint) { // Helper function to get updated sites IEnumerable <(TSite site, TBinding binding)> GetAllSites() => _client.WebSites. SelectMany(site => site.Bindings, (site, binding) => (site, binding)). ToList(); try { var allBindings = GetAllSites(); var bindingsUpdated = 0; var found = new List <IIISBinding>(); if (oldThumbprint != null) { var siteBindings = allBindings. Where(sb => StructuralComparisons.StructuralEqualityComparer.Equals(sb.binding.CertificateHash, oldThumbprint)). ToList(); // Update all bindings created using the previous certificate foreach (var(site, binding) in siteBindings) { try { // Only update if the old binding actually matches // with the new certificate if (identifiers.Any(i => Fits(binding, i, SSLFlags.None) > 0)) { found.Add(binding); if (UpdateBinding(site, binding, bindingOptions)) { bindingsUpdated += 1; } } else { _log.Warning( "Existing https binding {host}:{port}{ip} not updated because it doesn't seem to match the new certificate!", binding.Host, binding.Port, string.IsNullOrEmpty(binding.IP) ? "" : $":{binding.IP}"); } } catch (Exception ex) { _log.Error(ex, "Error updating binding {host}", binding.BindingInformation); throw; } } } // Find all hostnames which are not covered by any of the already updated // bindings yet, because we will want to make sure that those are accessable // in the target site var targetSite = _client.GetWebSite(bindingOptions.SiteId ?? -1); var todo = identifiers; while (todo.Any()) { // Filter by previously matched bindings todo = todo.Where(cert => !found.Any(iis => Fits(iis, cert, bindingOptions.Flags) > 0)); if (!todo.Any()) { break; } allBindings = GetAllSites(); var current = todo.First(); try { var(hostFound, bindings) = AddOrUpdateBindings( allBindings.Select(x => x.binding).ToArray(), targetSite, bindingOptions.WithHost(current.Value)); // Allow a single newly created binding to match with // multiple hostnames on the todo list, e.g. the *.example.com binding // matches with both a.example.com and b.example.com if (hostFound == null) { // We were unable to create the binding because it would // lead to a duplicate. Pretend that we did add it to // still be able to get out of the loop; found.Add(new DummyBinding(current)); } else { found.Add(hostFound); bindingsUpdated += bindings; } } catch (Exception ex) { _log.Error(ex, "Error creating binding {host}: {ex}", current, ex.Message); // Prevent infinite retry loop, we just skip the domain when // an error happens creating a new binding for it. User can // always change/add the bindings manually after all. found.Add(new DummyBinding(current)); } } return(bindingsUpdated); } catch (Exception ex) { _log.Error(ex, "Error installing"); throw; } }
/// <summary> /// Create or update a single binding in a single site /// </summary> /// <param name="site"></param> /// <param name="host"></param> /// <param name="flags"></param> /// <param name="thumbprint"></param> /// <param name="store"></param> /// <param name="port"></param> /// <param name="ipAddress"></param> /// <param name="fuzzy"></param> private (IIISBinding?, int) AddOrUpdateBindings(TBinding[] allBindings, TSite site, BindingOptions bindingOptions) { if (bindingOptions.Host == null) { throw new InvalidOperationException("bindingOptions.Host is null"); } _log.Verbose($"AddOrUpdateBindings {allBindings} bindings for host {bindingOptions.Host} on site {site.Name}"); // Require IIS manager to commit var commit = 0; // Get all bindings which could map to the host var matchingBindings = site.Bindings. Select(x => new { binding = x, fit = Fits(x, new DnsIdentifier(bindingOptions.Host), bindingOptions.Flags) }). Where(x => x.fit > 0). OrderByDescending(x => x.fit). ToList(); _log.Verbose($"Found {matchingBindings.Count} matching bindings"); // If there are any bindings if (matchingBindings.Any()) { var bestMatch = matchingBindings.First(); _log.Verbose($"Best match fit {bestMatch.fit}% for binding host {bestMatch.binding.Host}"); var bestMatches = matchingBindings.Where(x => x.binding.Host == bestMatch.binding.Host); _log.Verbose($"Found {bestMatches.Count()} best matches"); if (bestMatch.fit > 50 || !bindingOptions.Flags.HasFlag(SSLFlags.CentralSsl)) { // All existing https bindings var existing = bestMatches. Where(x => x.binding.Protocol == "https"). Select(x => x.binding.BindingInformation.ToLower()). ToList(); foreach (var match in bestMatches) { _log.Verbose($"Match binding protocol is {match.binding.Protocol}"); bool isHttps = match.binding.Protocol == "https"; if (isHttps) { if (UpdateExistingBindingFlags(bindingOptions.Flags, match.binding, allBindings, out var updateFlags)) { var updateOptions = bindingOptions.WithFlags(updateFlags); if (UpdateBinding(site, match.binding, updateOptions)) { commit++; } } } else { var addOptions = bindingOptions.WithHost(match.binding.Host); // The existance of an HTTP binding with a specific IP overrules // the default IP. if (addOptions.IP == IISClient.DefaultBindingIp && match.binding.IP != IISClient.DefaultBindingIp && !string.IsNullOrEmpty(match.binding.IP)) { addOptions = addOptions.WithIP(match.binding.IP); } var binding = addOptions.Binding; if (!existing.Contains(binding.ToLower()) && AllowAdd(addOptions, allBindings)) { AddBinding(site, addOptions); existing.Add(binding); commit++; } } } if (commit > 0) { return(bestMatch.binding, commit); } } } // At this point we haven't even found a partial match for our hostname // so as the ultimate step we create new https binding if (AllowAdd(bindingOptions, allBindings)) { var newBinding = AddBinding(site, bindingOptions); commit++; return(newBinding, commit); } // We haven't been able to do anything return(null, commit); }
/// <summary> /// Update/create bindings for all host names in the certificate /// </summary> /// <param name="target"></param> /// <param name="flags"></param> /// <param name="thumbprint"></param> /// <param name="store"></param> public void AddOrUpdateBindings(IEnumerable <string> identifiers, BindingOptions bindingOptions, byte[] oldThumbprint) { // Helper function to get updated sites IEnumerable <(IISSiteWrapper site, Binding binding)> GetAllSites() => WebSites. SelectMany(site => site.Site.Bindings, (site, binding) => (site, binding)). ToList(); try { var allBindings = GetAllSites(); var bindingsUpdated = 0; var found = new List <string>(); if (oldThumbprint != null) { var siteBindings = allBindings. Where(sb => StructuralComparisons.StructuralEqualityComparer.Equals(sb.binding.CertificateHash, oldThumbprint)). ToList(); // Update all bindings created using the previous certificate foreach (var(site, binding) in siteBindings) { try { UpdateBinding(site.Site, binding, bindingOptions); found.Add(binding.Host); bindingsUpdated += 1; } catch (Exception ex) { _log.Error(ex, "Error updating binding {host}", binding.BindingInformation); throw; } } } // Find all hostnames which are not covered by any of the already updated // bindings yet, because we will want to make sure that those are accessable // in the target site var targetSite = GetWebSite(bindingOptions.SiteId ?? -1); IEnumerable <string> todo = identifiers; while (todo.Any()) { // Filter by previously matched bindings todo = todo.Where(cert => !found.Any(iis => Fits(iis, cert, bindingOptions.Flags) > 0)); if (!todo.Any()) { break; } allBindings = GetAllSites(); var current = todo.First(); try { var binding = AddOrUpdateBindings( allBindings.Select(x => x.binding).ToArray(), targetSite, bindingOptions.WithHost(current), !bindingOptions.Flags.HasFlag(SSLFlags.CentralSSL)); // Allow a single newly created binding to match with // multiple hostnames on the todo list, e.g. the *.example.com binding // matches with both a.example.com and b.example.com if (binding == null) { // We were unable to create the binding because it would // lead to a duplicate. Pretend that we did add it to // still be able to get out of the loop; found.Add(current); } else { found.Add(binding); bindingsUpdated += 1; } } catch (Exception ex) { _log.Error(ex, "Error creating binding {host}: {ex}", current, ex.Message); // Prevent infinite retry loop, we just skip the domain when // an error happens creating a new binding for it. User can // always change/add the bindings manually after all. found.Add(current); } } if (bindingsUpdated > 0) { _log.Information("Committing {count} {type} binding changes to IIS", bindingsUpdated, "https"); Commit(); } else { _log.Warning("No bindings have been changed"); } } catch (Exception ex) { _log.Error(ex, "Error installing"); throw; } }
/// <summary> /// Create or update a single binding in a single site /// </summary> /// <param name="site"></param> /// <param name="host"></param> /// <param name="flags"></param> /// <param name="thumbprint"></param> /// <param name="store"></param> /// <param name="port"></param> /// <param name="ipAddress"></param> /// <param name="fuzzy"></param> private string AddOrUpdateBindings(Binding[] allBindings, IISSiteWrapper site, BindingOptions bindingOptions, bool fuzzy) { // Get all bindings which could map to the host var matchingBindings = site.Site.Bindings. Select(x => new { binding = x, fit = Fits(x.Host, bindingOptions.Host, bindingOptions.Flags) }). Where(x => x.fit > 0). OrderByDescending(x => x.fit). ToList(); var httpsMatches = matchingBindings.Where(x => x.binding.Protocol == "https"); var httpMatches = matchingBindings.Where(x => x.binding.Protocol == "http"); // Existing https binding for exactly the domain we are looking for, will be // updated to use the new ACME certificate var perfectHttpsMatches = httpsMatches.Where(x => x.fit == 100); if (perfectHttpsMatches.Any()) { foreach (var perfectMatch in perfectHttpsMatches) { // The return value of UpdateFlags doesn't have to be checked here because // we have a perfect match, e.g. there is always a host name and thus // no risk when turning on the SNI flag UpdateFlags(bindingOptions.Flags, perfectMatch.binding, allBindings, out SSLFlags updateFlags); var updateOptions = bindingOptions.WithFlags(updateFlags); UpdateBinding(site.Site, perfectMatch.binding, updateOptions); } return(bindingOptions.Host); } // If we find a http-binding for the domain, a corresponding https binding // is set up to match incoming secure traffic var perfectHttpMatches = httpMatches.Where(x => x.fit == 100); if (perfectHttpMatches.Any()) { if (AllowAdd(bindingOptions, allBindings)) { AddBinding(site, bindingOptions); return(bindingOptions.Host); } } // Allow partial matching. Doesn't work for IIS CCS. if (bindingOptions.Host.StartsWith("*.") || fuzzy) { httpsMatches = httpsMatches.Except(perfectHttpsMatches); httpMatches = httpMatches.Except(perfectHttpMatches); // There are no perfect matches for the domain, so at this point we start // to look at wildcard and/or default bindings binding. Since they are // order by 'best fit' we look at the first one. if (httpsMatches.Any()) { foreach (var match in httpsMatches) { if (UpdateFlags(bindingOptions.Flags, match.binding, allBindings, out SSLFlags updateFlags)) { var updateOptions = bindingOptions.WithFlags(updateFlags); UpdateBinding(site.Site, match.binding, updateOptions); return(match.binding.Host); } } } // Nothing on https, then start to look at http if (httpMatches.Any()) { var bestMatch = httpMatches.First(); var addOptions = bindingOptions.WithHost(bestMatch.binding.Host); if (AllowAdd(addOptions, allBindings)) { AddBinding(site, addOptions); return(bestMatch.binding.Host); } } } // At this point we haven't even found a partial match for our hostname // so as the ultimate step we create new https binding if (AllowAdd(bindingOptions, allBindings)) { AddBinding(site, bindingOptions); return(bindingOptions.Host); } return(null); }
/// <summary> /// Create or update a single binding in a single site /// </summary> /// <param name="site"></param> /// <param name="host"></param> /// <param name="flags"></param> /// <param name="thumbprint"></param> /// <param name="store"></param> /// <param name="port"></param> /// <param name="ipAddress"></param> /// <param name="fuzzy"></param> private string?AddOrUpdateBindings(TBinding[] allBindings, TSite site, BindingOptions bindingOptions) { if (bindingOptions.Host == null) { throw new InvalidOperationException("bindingOptions.Host is null"); } // Get all bindings which could map to the host var matchingBindings = site.Bindings. Select(x => new { binding = x, fit = Fits(x.Host, bindingOptions.Host, bindingOptions.Flags) }). Where(x => x.fit > 0). OrderByDescending(x => x.fit). ToList(); // If there are any bindings if (matchingBindings.Any()) { var bestMatch = matchingBindings.First(); var bestMatches = matchingBindings.Where(x => x.binding.Host == bestMatch.binding.Host); if (bestMatch.fit == 100 || !bindingOptions.Flags.HasFlag(SSLFlags.CentralSsl)) { // All existing https bindings var existing = bestMatches. Where(x => x.binding.Protocol == "https"). Select(x => x.binding.BindingInformation). ToList(); foreach (var match in bestMatches) { var isHttps = match.binding.Protocol == "https"; if (isHttps) { if (UpdateExistingBindingFlags(bindingOptions.Flags, match.binding, allBindings, out var updateFlags)) { var updateOptions = bindingOptions.WithFlags(updateFlags); UpdateBinding(site, match.binding, updateOptions); } } else { var addOptions = bindingOptions.WithHost(match.binding.Host); // The existance of an HTTP binding with a specific IP overrules // the default IP. if (addOptions.IP == IISClient.DefaultBindingIp && match.binding.IP != IISClient.DefaultBindingIp && !string.IsNullOrEmpty(match.binding.IP)) { addOptions = addOptions.WithIP(match.binding.IP); } var binding = addOptions.Binding; if (!existing.Contains(binding) && AllowAdd(addOptions, allBindings)) { AddBinding(site, addOptions); existing.Add(binding); } } } return(bestMatch.binding.Host); } } // At this point we haven't even found a partial match for our hostname // so as the ultimate step we create new https binding if (AllowAdd(bindingOptions, allBindings)) { AddBinding(site, bindingOptions); return(bindingOptions.Host); } // We haven't been able to do anything return(null); }