예제 #1
0
        /// <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;
            }
        }
예제 #2
0
        /// <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);
        }
예제 #3
0
        /// <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;
            }
        }
예제 #4
0
        /// <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);
        }
예제 #5
0
        /// <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);
        }