/// <summary> /// Create a new binding /// </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="IP"></param> private void AddBinding(TSite site, BindingOptions options) { options = options.WithFlags(CheckFlags(true, options.Host, options.Flags)); _log.Information(true, "Adding new https binding {binding}", options.Binding); _client.AddBinding(site, options); }
/// <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) { // 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(); 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 UpdateExistingBindingFlags(bindingOptions.Flags, perfectMatch.binding, allBindings, out SSLFlags updateFlags); var updateOptions = bindingOptions.WithFlags(updateFlags); UpdateBinding(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.Flags.HasFlag(SSLFlags.CentralSSL)) { 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 (UpdateExistingBindingFlags(bindingOptions.Flags, match.binding, allBindings, out SSLFlags updateFlags)) { var updateOptions = bindingOptions.WithFlags(updateFlags); UpdateBinding(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?, bool) AddOrUpdateBindings(TBinding[] allBindings, TSite site, BindingOptions bindingOptions) { if (bindingOptions.Host == null) { throw new InvalidOperationException("bindingOptions.Host is null"); } // Require IIS manager to commit var commitRequired = false; // 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.ToLower()). 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); commitRequired = 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.ToLower()) && AllowAdd(addOptions, allBindings)) { AddBinding(site, addOptions); existing.Add(binding); commitRequired = true; } } } return(bestMatch.binding.Host, commitRequired); } } // 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); commitRequired = true; return(bindingOptions.Host, commitRequired); } // We haven't been able to do anything return(null, commitRequired); }
/// <summary> /// Update existing bindng /// </summary> /// <param name="site"></param> /// <param name="existingBinding"></param> /// <param name="flags"></param> /// <param name="thumbprint"></param> /// <param name="store"></param> private void UpdateBinding(Site site, Binding existingBinding, BindingOptions options) { // Check flags options = options.WithFlags(CheckFlags(existingBinding.Host, options.Flags)); // IIS 7.x is very picky about accessing the sslFlags attribute var currentFlags = existingBinding.SSLFlags(); if ((currentFlags & ~SSLFlags.SNI) == (options.Flags & ~SSLFlags.SNI) && // Don't care about SNI status ((options.Store == null && existingBinding.CertificateStoreName == null) || StructuralComparisons.StructuralEqualityComparer.Equals(existingBinding.CertificateHash, options.Thumbprint) && string.Equals(existingBinding.CertificateStoreName, options.Store, StringComparison.InvariantCultureIgnoreCase))) { _log.Verbose("No binding update needed"); } else { _log.Information(true, "Updating existing https binding {host}:{port}", existingBinding.Host, existingBinding.EndPoint.Port); // Replace instead of change binding because of #371 var handled = new[] { "protocol", "bindingInformation", "sslFlags", "certificateStoreName", "certificateHash" }; var replacement = site.Bindings.CreateElement("binding"); replacement.Protocol = existingBinding.Protocol; replacement.BindingInformation = existingBinding.BindingInformation; replacement.CertificateStoreName = options.Store; replacement.CertificateHash = options.Thumbprint; foreach (var attr in existingBinding.Attributes) { try { if (!handled.Contains(attr.Name) && attr.Value != null) { replacement.SetAttributeValue(attr.Name, attr.Value); } } catch (Exception ex) { _log.Warning("Unable to set attribute {name} on new binding: {ex}", attr.Name, ex.Message); } } // If current binding has SNI, the updated version // will also have that flag set, regardless // of whether or not it was requested by the caller. // Callers should not generally request SNI unless // required for the binding, e.g. for TLS-SNI validation. // Otherwise let the admin be in control. if (currentFlags.HasFlag(SSLFlags.SNI)) { options = options.WithFlags(options.Flags | SSLFlags.SNI); } if (options.Flags > 0) { replacement.SetAttributeValue("sslFlags", options.Flags); } site.Bindings.Remove(existingBinding); site.Bindings.Add(replacement); } }
/// <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) { var updateOptions = bindingOptions.WithFlags(UpdateFlags(bindingOptions.Flags, perfectMatch.binding, allBindings)); 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. Also // should not be used for TLS-SNI validation. 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()) { var bestMatch = httpsMatches.First(); var updateFlags = UpdateFlags(bindingOptions.Flags, bestMatch.binding, allBindings); var updateOptions = bindingOptions.WithFlags(updateFlags); UpdateBinding(site.Site, bestMatch.binding, updateOptions); return(bestMatch.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 a new binding /// </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="IP"></param> private IIISBinding AddBinding(TSite site, BindingOptions options) { options = options.WithFlags(CheckFlags(true, options.Host, options.Flags)); _log.Information(LogType.All, "Adding new https binding {binding}", options.Binding); return(_client.AddBinding(site, options)); }