예제 #1
0
        /// <summary>
        ///
        /// </summary>
        /// <param name="rule">The rule</param>
        /// <param name="replaceIfExists">If a rule exists with the same name, replace it, otherwise, throw exception.</param>
        public void AddRewriteRule(string rule, bool replaceIfExists = true)
        {
            var parsedRule = XElement.Parse(rule);

            string name = parsedRule.DescendantsAndSelf("rule").First().Attribute("name").Value;

            var       webconfigfilepath = this.GetCdnWebConfigPathInitialized();
            XDocument webcfg            = XDocument.Parse(File.ReadAllText(webconfigfilepath));

            XElement ruleselement = webcfg.Root.GetAndEnsureXpath("system.webServer/rewrite/rules");

            var existingRule = (from p in ruleselement.Descendants("rule")
                                where p.Attribute("name")?.Value == name
                                select p).FirstOrDefault();

            if (existingRule != null && !replaceIfExists)
            {
                throw new Exception("Rule already exists");
            }

            existingRule?.Remove();

            // Now add from scratchrule
            ruleselement.Add(parsedRule);

            // Perist it
            UtilsIis.WriteWebConfig(webconfigfilepath, webcfg.ToString());
        }
예제 #2
0
        /// <summary>
        /// Remove all rewrite rules that start with the given prefix.
        /// </summary>
        /// <param name="prefix"></param>
        /// <param name="logger"></param>
        public void RemoveRewriteRulesWithPrefix(string prefix, ILoggerInterface logger)
        {
            // If there is no CDN site, do nothing
            using (ServerManager manager = new ServerManager())
            {
                var site = UtilsIis.FindSiteWithName(manager, this.CstChefCndSiteName, logger).SingleOrDefault();

                if (site == null)
                {
                    return;
                }
            }

            var       webconfigfilepath = this.GetCdnWebConfigPathInitialized();
            XDocument webcfg            = XDocument.Parse(File.ReadAllText(webconfigfilepath));

            var rules = (from p in webcfg.Descendants("rule")
                         where p.Attribute("name")?.Value?.StartsWith(prefix) == true
                         select p).ToList();

            foreach (var rule in rules)
            {
                rule?.Remove();
            }

            UtilsIis.WriteWebConfig(webconfigfilepath, webcfg.ToString());
        }
예제 #3
0
        /// <summary>
        /// Get an instance of SslCertificateProviderService
        /// </summary>
        public SslCertificateProviderService(
            ILoggerInterface logger,
            string appId,
            EnvironmentSettings globalSettings,
            Deployment deployment)
        {
            this.AppPoolUtils    = new UtilsAppPool(logger);
            this.UtilsHosts      = new UtilsHosts(logger);
            this.MockEnvironment = UtilsSystem.RunningInContinuousIntegration() || UnitTestDetector.IsRunningInTests || Debugger.IsAttached;

            // Everything performed against the staging API needs to be kept apart, including signer, etc...
            this.StoragePath = Path.Combine(globalSettings.GetDefaultContentStorage().path, "letsencrypt" + (this.MockEnvironment ? "_mock" : null));
            UtilsSystem.EnsureDirectoryExists(this.StoragePath, true);

            // If CCS is available, use that, otherwise use the central content storage
            string sslRenewalStateStorePath = UtilsIis.CentralStoreEnabled()
                ? UtilsIis.CentralStorePath(logger)
                : globalSettings.GetDefaultContentStorage().path;

            this.SimpleStoreRenewalStatus = new SimpleStore(Path.Combine(sslRenewalStateStorePath, "_ssl_renewal_state_store"), true);

            this.Logger         = logger;
            this.AppId          = appId;
            this.Deployment     = deployment;
            this.GlobalSettings = globalSettings;
        }
        public void TestIpParser()
        {
            Assert.Equal("192.168.5.3", UtilsIis.GetSubnetAndMaskFromCidr("192.168.5.3/20").Item1.ToString());
            Assert.Equal("255.255.240.0", UtilsIis.GetSubnetAndMaskFromCidr("192.168.5.3/20").Item2.ToString());

            Assert.Equal("192.168.5.3", UtilsIis.GetSubnetAndMaskFromCidr("192.168.5.3").Item1.ToString());
            Assert.Null(UtilsIis.GetSubnetAndMaskFromCidr("192.168.5.3").Item2);
        }
예제 #5
0
        /// <summary>
        /// Deploy's the IIS PHP handler at the application hosts level, but site specific.
        /// </summary>
        protected void deployIISHandlers()
        {
            using (ServerManager serverManager = new ServerManager())
            {
                var siteName = this.Deployment.getShortId();

                string siteAlias = this.Deployment.getShortId();

                // fastCgi settings in IIS can only be set at the HOSTS level
                // we found no way to set this at a web.config level.
                Microsoft.Web.Administration.Configuration config = serverManager.GetApplicationHostConfiguration();

                ConfigurationElement cfs = null;

                ConfigurationSection           section;
                ConfigurationElementCollection elems;

                section = config.GetSection("system.webServer/handlers", siteName);
                elems   = section.GetCollection();

                cfs = elems.CreateElement("add");

                cfs.SetAttributeValue("name", "php-" + this.Deployment.getShortId());
                cfs.SetAttributeValue("path", "*.php");
                cfs.SetAttributeValue("verb", "GET,HEAD,POST,PUT,PATCH,DELETE");
                cfs.SetAttributeValue("modules", "FastCgiModule");
                cfs.SetAttributeValue("scriptProcessor", this.GetFastCgiExe() + "|" + this.Deployment.getShortId());
                cfs.SetAttributeValue("resourceType", "Either");
                cfs.SetAttributeValue("requireAccess", "Script");
                cfs.SetAttributeValue("responseBufferLimit", 0);

                // Add as the first handler... order matters here.
                elems.AddAt(0, cfs);

                // And index.php as a default document...
                var files = config.GetSection("system.webServer/defaultDocument", siteName).GetChildElement("files");
                elems = files.GetCollection();

                // We might have inherited settings from a higher level
                // that already cover the default document configuration.
                var exists = (from p in elems
                              where p.Schema.Name == "add" &&
                              ((string)p.GetAttributeValue("value")) == "index.php"
                              select 1).Any();

                if (!exists)
                {
                    // TODO: This fails if the default document is already configured at a higher level. Ensure it does
                    // not exist before trying to create it!
                    cfs = elems.CreateElement("add");
                    cfs.SetAttributeValue("value", "index.php");
                    elems.Add(cfs);
                }

                UtilsIis.CommitChanges(serverManager);
            }
        }
        protected override void ProcessRecord()
        {
            ConsoleUtils.RunCode(() =>
            {
                var logger = new logger.ConsoleLogger();
                logger.SetVerbose(true);

                UtilsIis.EnsureCertificateInCentralCertificateStoreIsRebound(this.Hostname, logger);
            });
        }
예제 #7
0
        public void ConfigureProxy()
        {
            // For this local CDN to work we need IIS-ARR installed and configured at the IIS level, otherwise
            // IIS gets stuck with this config (+ it won't work)
            try
            {
                // Ensure that proxy is enabled and available at the IIS level.
                // This needs the IIS Application Request Routing extension.
                using (ServerManager manager = new ServerManager())
                {
                    bool configChanged = false;

                    var config = manager.GetApplicationHostConfiguration();

                    ConfigurationSection proxySection = config.GetSection("system.webServer/proxy");

                    // Disable reverseRewriteHostInResponseHeaders
                    if (!bool.TryParse(proxySection["reverseRewriteHostInResponseHeaders"]?.ToString(), out var proxyReverseRewrite) || proxyReverseRewrite == true)
                    {
                        proxySection["reverseRewriteHostInResponseHeaders"] = false;
                        configChanged = true;
                    }

                    // Enable proxy functionality
                    if (!bool.TryParse(proxySection["enabled"]?.ToString(), out var proxyEnabled) || proxyEnabled == false)
                    {
                        proxySection["enabled"] = true;
                        configChanged           = true;
                    }

                    // Disable disk cache
                    ConfigurationElement cacheElement = proxySection.GetChildElement("cache");

                    if (!bool.TryParse(cacheElement["enabled"]?.ToString(), out var cacheEnabled) || cacheEnabled == true)
                    {
                        cacheElement["enabled"] = false;
                        configChanged           = true;
                    }

                    if (configChanged)
                    {
                        this.Logger.LogWarning(false, "Your IIS-ARR settings have been updated to work with Chef CDN: [proxy.enabled=true] && [proxy.cache.enabled=false]");
                        UtilsIis.CommitChanges(manager);
                    }
                }
            }
            catch (Exception e)
            {
                throw new Exception("Could not configure server-wide proxy settings for CDN related functionality. Make sure that the chocolatey iis-arr package is installed.", e);
            }
        }
예제 #8
0
        /// <summary>
        /// Move a rule to the requested INDEX in the set of rules
        /// </summary>
        /// <param name="ruleName"></param>
        /// <param name="index"></param>
        protected void SetRuleIndex(string ruleName, int index)
        {
            var webconfigfilepath = this.GetCdnWebConfigPathInitialized();

            XDocument webcfg = XDocument.Parse(File.ReadAllText(webconfigfilepath));

            XElement ruleselement = webcfg.Root.GetAndEnsureXpath("system.webServer/rewrite/rules");

            XElement existingRule = null;

            var nodes = ruleselement.Nodes().ToList();

            foreach (var node in nodes.ToList())
            {
                if (node is XElement xNode && xNode.Attribute("name")?.Value == ruleName)
                {
                    if (nodes.IndexOf(xNode) == index)
                    {
                        return;
                    }

                    existingRule = xNode;
                    nodes.Remove(xNode);
                    break;
                }
            }

            if (existingRule == null)
            {
                throw new Exception($"Rule {ruleName} not found.");
            }

            nodes.Insert(index, existingRule);

            ruleselement.ReplaceAll(nodes.ToArray());

            UtilsIis.WriteWebConfig(webconfigfilepath, webcfg.ToString());
        }
예제 #9
0
        /// <summary>
        /// Get the webroot for the CDN site, initialized with a base web.config prepared for URL REWRITING
        /// </summary>
        /// <returns></returns>
        public string GetCdnWebConfigPathInitialized()
        {
            var basedir =
                UtilsSystem.EnsureDirectoryExists(
                    UtilsSystem.CombinePaths(this.GlobalSettings.GetDefaultApplicationStorage().path, "__chef_cdn"), true);

            var webconfigfilepath = UtilsSystem.CombinePaths(basedir, "web.config");

            // Si no hay un web.config plantilla, crearlo ahora.
            if (!File.Exists(webconfigfilepath))
            {
                File.WriteAllText(webconfigfilepath, @"
                        <configuration>
                          <system.webServer>
                            <rewrite>
                              <rules>
                              </rules>
                              <outboundRules>
                              </outboundRules>
                            </rewrite>
                          </system.webServer>
                        </configuration>
                        ");
            }

            UtilsWindowsAccounts.AddPermissionToDirectoryIfMissing(new SecurityIdentifier(UtilsWindowsAccounts.WELL_KNOWN_SID_USERS), basedir, FileSystemRights.ReadAndExecute);

            // Make sure that the site exists
            using (ServerManager manager = new ServerManager())
            {
                bool configChanged = false;

                var site = UtilsIis.FindSiteWithName(manager, this.CstChefCndSiteName, this.Logger)
                           .FirstOrDefault();

                if (site == null)
                {
                    manager.Sites.Add(this.CstChefCndSiteName, "http", $"{UtilsIis.LOCALHOST_ADDRESS}:80:{this.CstChefInternalHostname}", basedir);

                    configChanged = true;
                }
                else
                {
                    if (site.Applications.First().VirtualDirectories.First().PhysicalPath != basedir)
                    {
                        site.Applications.First().VirtualDirectories.First().PhysicalPath = basedir;
                        configChanged = true;
                    }
                }

                if (configChanged)
                {
                    UtilsIis.CommitChanges(manager);
                }
            }

            this.UtilsHosts.AddHostsMapping(UtilsIis.LOCALHOST_ADDRESS, this.CstChefInternalHostname, "chf_IISDeployer_CDN");

            // Add a cross domain file
            var crossdomainfilepath = UtilsSystem.CombinePaths(Path.GetDirectoryName(webconfigfilepath), "crossdomain.xml");

            File.WriteAllText(crossdomainfilepath, UtilsSystem.GetEmbededResourceAsString(Assembly.GetExecutingAssembly(), "IIS.crossdomain.xml"));

            // Add common proxy headers
            UtilsIis.AddAllowedServerVariablesForUrlRewrite(
                this.CstChefCndSiteName,
                "HTTP_X_FORWARDED_FOR",
                "HTTP_X_FORWARDED_PROTO",
                "HTTP_X_FORWARDED_HOST");

            return(webconfigfilepath);
        }
예제 #10
0
        /// <summary>
        /// Provisions a certificate in the central store
        /// </summary>
        /// <param name="hostName">Domain to register</param>
        /// <param name="email">Registration e-mail</param>
        /// <param name="bindingInfo">IIS binding info</param>
        /// <param name="ownerSiteName">The site that owns the binding, used to assign identity and application pool permissions.</param>
        /// <param name="forceSelfSigned">Force a self-signed certificate</param>
        /// <param name="forceRenewal">Force renewal, even if renewal conditions are not met</param>
        /// <returns>The certificate's friendly name, ready to be bound in IIS</returns>
        public void ProvisionCertificateInIis(
            string hostName,
            string email,
            string bindingInfo,
            string ownerSiteName,
            bool forceSelfSigned = false,
            bool forceRenewal    = false)
        {
            if (hostName.Contains("*"))
            {
                throw new Exception($"Provisioning certificates for wildcard host name '{hostName}' is not supported.");
            }

            var    currentCertificate       = UtilsIis.FindCertificateInCentralCertificateStore(hostName, this.Logger, out _);
            double remainingCertificateDays = (currentCertificate?.NotAfter - DateTime.Now)?.TotalDays ?? 0;

            this.Logger.LogInfo(true, "Total days remaining for certificate expiration: '{0}'", (int)Math.Floor(remainingCertificateDays));

            // Trigger renovation. Do this differently on mock/prod environment.
            // Next renewal attempt is calculated based on previous renewal attempt
            var renewalState = this.GetCertificateRenewalState(hostName);

            // Legacy apps don't have this set, or when a certificate has been manually placed
            if (renewalState.NextRenewal == null && remainingCertificateDays > 1)
            {
                renewalState.NextRenewal = this.CalculateNextRenewalAttempt(hostName, (int)remainingCertificateDays);
                this.StoreCertificateRenewalState(renewalState);
            }

            int remainingDaysForNextRenewal = renewalState.NextRenewal == null ? 0 : (int)(renewalState.NextRenewal - DateTime.UtcNow).Value.TotalDays;
            int certificateTotalDuration    = currentCertificate == null ? 0 : (int)(currentCertificate.NotAfter - currentCertificate.NotBefore).TotalDays;

            this.Logger.LogInfo(true, "Next renewal attempt for this site SSL targeted in '{0}' days.", remainingDaysForNextRenewal);

            // Check that the validationfailed request rate is not exceeded for this domain
            if (!forceRenewal && renewalState.FailedValidations.AsIterable().Count(i => (DateTime.UtcNow - i).TotalHours < 48) >= 2)
            {
                // Make this message verbos so that it will not flood the logs, the failed validation message will get logged
                // anyways and is sufficient.
                this.Logger.LogWarning(true, "The hostname '{0}' has reached the limit of two failed validations in the last 48 hours.", hostName);
                return;
            }

            if (!forceRenewal && !forceSelfSigned && remainingDaysForNextRenewal > 0 && remainingCertificateDays > 0)
            {
                this.Logger.LogWarning(true, "Next renewal attempt date not reached, skipping SSL provisioning.");
                return;
            }

            if (!forceRenewal && remainingDaysForNextRenewal > 0 && (remainingDaysForNextRenewal > certificateTotalDuration * 0.5) && certificateTotalDuration > 0)
            {
                this.Logger.LogWarning(false, "Certificate has not yet been through at least 50% of it's lifetime so it will not be renewed.'");
                renewalState.NextRenewal = this.CalculateNextRenewalAttempt(hostName, (int)remainingCertificateDays);
                this.StoreCertificateRenewalState(renewalState);
                return;
            }

            // Check the general too many requests rate exceeded
            if (!forceRenewal && this.SimpleStoreRenewalStatus.Get <bool>("ssl-certificate-provider-too-many-requests", out var tooManyRequests))
            {
                this.Logger.LogWarning(false, "Certificate provisioning temporarily disabled due to a Too Many Requests ACME error. Flag stored in {0}", tooManyRequests.StorePath);
                return;
            }

            this.Logger.LogInfo(false, "Attempting SSL certificate renewal for site '{0}' and host '{1}'", ownerSiteName, hostName);

            // Clear old validation failed requests
            if (renewalState.FailedValidations?.Any() == true)
            {
                // Only keep failed validations that happen in the last 5 days
                renewalState.FailedValidations = renewalState.FailedValidations
                                                 .Where((i) => (DateTime.UtcNow - i).TotalDays < 5).ToList();
            }

            // This is a little bit inconvenient but... the most reliable and compatible
            // way to do this is to setup a custom IIS website that uses the binding during
            // provisioning.

            long        tempSiteId;
            List <Site> haltedSites = new List <Site>();

            var    tempSiteName  = "cert-" + this.AppId;
            var    tempSiteAppId = "cert-" + this.AppId;
            string tempHostName  = "localcert-" + hostName;

            this.Logger.LogInfo(true, "Preparing temp site: " + tempSiteName);

            List <Site> conflictingSites;

            // Prepare the site
            using (ServerManager sm = new ServerManager())
            {
                // Query the sites in a resilient way...
                conflictingSites = UtilsSystem.QueryEnumerable(
                    sm.Sites,
                    (s) => s.State == ObjectState.Started && s.Bindings.Any((i) => i.Host.Equals(hostName)),
                    (s) => s,
                    (s) => s.Name,
                    this.Logger);
            }

            // Stop the sites that might prevent this one from starting
            foreach (var s in conflictingSites)
            {
                this.Logger.LogInfo(true, "Stopping site {0} to avoid binding collision.", s.Name);
                this.AppPoolUtils.WebsiteAction(s.Name, AppPoolActionType.Stop, skipApplicationPools: true);
                haltedSites.Add(s);
            }

            using (ServerManager sm = new ServerManager())
            {
                // Make sure there is no other site (might be stuck?)
                var existingSite = (from p in sm.Sites
                                    where p.Name == tempSiteName
                                    select p).FirstOrDefault();

                var tempSite = existingSite ?? sm.Sites.Add(tempSiteName, this.GetAcmeTemporarySiteRootForApplication(), 80);

                // Propagate application pool usage so that permissions are properly handled.
                var ownerSite = sm.Sites.First((i) => i.Name == ownerSiteName);
                tempSite.Applications.First().ApplicationPoolName = ownerSite.Applications.First().ApplicationPoolName;

                // Delete all bindings
                tempSite.Bindings.Clear();

                tempSite.Bindings.Add(bindingInfo, "http");
                tempSite.Bindings.Add($"{UtilsIis.LOCALHOST_ADDRESS}:80:" + tempHostName, "http");
                tempSiteId = tempSite.Id;

                this.UtilsHosts.AddHostsMapping(UtilsIis.LOCALHOST_ADDRESS, tempHostName, tempSiteAppId);

                // Prepare the website contents
                var sourceDir = UtilsSystem.FindResourcePhysicalPath(typeof(IISDeployer), ".well-known");
                UtilsSystem.CopyFilesRecursively(new DirectoryInfo(sourceDir), new DirectoryInfo(this.GetWellKnownSharedPathForApplication()), true);

                UtilsIis.CommitChanges(sm);
            }

            UtilsIis.WaitForSiteToBeAvailable(tempSiteName, this.Logger);
            UtilsIis.ConfigureAnonymousAuthForIisApplication(tempSiteName, this.Deployment.WindowsUsernameFqdn(), this.Deployment.GetWindowsPassword());

            IAcmeSharpProvider provider = null;

            try
            {
                this.AppPoolUtils.WebsiteAction(tempSiteName, AppPoolActionType.Start);

                // Check that the site does work using the local binding
                var testDataUrl = $"http://{tempHostName}/.well-known/acme-challenge/test.html";
                this.Logger.LogInfo(true, "Validating local challenge setup at: {0}", testDataUrl);

                if (!string.Equals(UtilsSystem.DownloadUriAsText(testDataUrl), "test data"))
                {
                    throw new Exception($"Could not locally validate acme challenge site setup at {testDataUrl}");
                }

                // Ssl registration configuration only depends on the e-mail and is signed as such
                string sslSignerAndRegistrationStoragePath = UtilsSystem.EnsureDirectoryExists(
                    UtilsSystem.CombinePaths(this.StoragePath, "_ssl_config", StringFormating.Instance.ExtremeClean(email)), true);

                // Initialize the provider
                bool useMockProvider = this.MockEnvironment || forceSelfSigned;

                provider = useMockProvider
                    ? (IAcmeSharpProvider) new AcmeSharpProviderMock(this.Logger, tempHostName)
                    : this.GetAcmeProvider(this.Logger, hostName);

                var signerPath       = Path.Combine(this.StoragePath, "_signer.xml");
                var registrationPath = Path.Combine(sslSignerAndRegistrationStoragePath, "registration.json");

                provider.InitRegistration(signerPath, registrationPath, email);

                string challengeUrl;
                string challengeContent;
                string challengeFilePath;

                try
                {
                    provider.GenerateHttpChallenge(
                        out challengeUrl,
                        out challengeContent,
                        out challengeFilePath);
                }
                catch (AcmeClient.AcmeWebException acmeException)
                {
                    if (acmeException.Message.Contains("429"))
                    {
                        int waitHours = 1;
                        this.SimpleStoreRenewalStatus.Set("ssl-certificate-provider-too-many-requests", true, 60 * waitHours);
                        this.Logger.LogError("Let's encrypt too many requests issue. Certificate provisioning disabled for the next {0} hours.", waitHours);
                        this.Logger.LogException(acmeException, EventLogEntryType.Warning);
                        return;
                    }

                    throw;
                }

                // Write the challanege contents
                string challengeFullPath =
                    Path.Combine(this.GetAcmeTemporarySiteRootForApplication(), challengeFilePath);

                File.WriteAllText(
                    challengeFullPath,
                    challengeContent);

                this.Logger.LogInfo(false, $"Veryfing challenge at '{challengeUrl}'");

                try
                {
                    // Validate that we can actually access the challenge ourselves!
                    string contents = UtilsSystem.DownloadUriAsText(challengeUrl, false);

                    if (!string.Equals(contents, challengeContent))
                    {
                        throw new Exception(
                                  $"Could not validate ACME challenge, retrieved challenge '{contents}' does not match '{challengeContent}'");
                    }
                }
                catch (Exception e)
                {
                    this.Logger.LogWarning(true, "Cannot self-verify auth challenge, this can sometimes happeen under some DNS setups. {0}", e.Message + Environment.NewLine + e.InnerException?.Message);
                }

                var challengeValidated = false;

                try
                {
                    challengeValidated = provider.ValidateChallenge();
                }
                catch (Exception e)
                {
                    this.Logger.LogException(e, EventLogEntryType.Warning);
                }

                this.Logger.LogWarning(true, "Remote challenge validation success: " + (challengeValidated ? "Yes" : "No"));

                // Download the certificates to this temp location
                string           temporaryCertificatePath = UtilsSystem.EnsureDirectoryExists(UtilsSystem.CombinePaths(this.StoragePath, this.AppId, "ssl_certificates", hostName), true);
                CertificatePaths certificatepaths         = null;

                // This is here for testing purposes
                if (Environment.GetEnvironmentVariable("TEST_FAIL_CHALLENGE_VALIDATION") == true.ToString())
                {
                    challengeValidated = false;
                }

                if (!challengeValidated)
                {
                    // There is a Failed Validation limit of 5 failures per account, per hostname, per hour.
                    renewalState.FailedValidations = renewalState.FailedValidations ?? new List <DateTime>();
                    renewalState.FailedValidations.Add(DateTime.UtcNow);

                    this.Logger.LogError(
                        "Challenge could not be validated at '{0}'. If behind a load balancer, make sure that the site is deployed in ALL nodes, remove the self-signed certificate from the store and redeploy the application.",
                        challengeUrl);

                    this.StoreCertificateRenewalState(renewalState);
                }
                else
                {
                    try
                    {
                        certificatepaths = provider.DownloadCertificate(
                            UtilsEncryption.GetMD5(hostName),
                            hostName,
                            temporaryCertificatePath);
                    }
                    catch (AcmeClient.AcmeWebException acmeException)
                    {
                        this.Logger.LogException(acmeException, EventLogEntryType.Warning);
                    }
                    catch (WebException webException)
                    {
                        this.Logger.LogException(webException, EventLogEntryType.Warning);
                    }
                    catch (Exception e)
                    {
                        this.Logger.LogException(e, EventLogEntryType.Warning);
                    }
                }

                if (certificatepaths == null && currentCertificate == null)
                {
                    this.Logger.LogWarning(false, "Unable to acquire certificate and site does not have a valid existing one, using self-signed fallback.");

                    provider = new AcmeSharpProviderMock(this.Logger, hostName);

                    certificatepaths = provider.DownloadCertificate(
                        UtilsEncryption.GetMD5(hostName),
                        hostName,
                        temporaryCertificatePath);
                }

                // Save this, use a fixed name certificate file
                if (certificatepaths != null)
                {
                    string certificateFilePath = Path.Combine(UtilsIis.CentralStorePath(this.Logger), hostName + ".pfx");

                    UtilsSystem.RetryWhile(
                        () => { File.Copy(certificatepaths.pfxPemFile, certificateFilePath, true); },
                        (e) => true,
                        2500,
                        this.Logger);

                    this.Logger.LogInfo(false, "Certificate file writen to '{0}'", certificateFilePath);

                    // TODO: Activate this refreshing when it's prooved to work
                    // UtilsIis.EnsureCertificateInCentralCertificateStoreIsRebound(hostName, this.Logger);
                }

                // Remove temporary certificates
                UtilsSystem.DeleteDirectory(temporaryCertificatePath, this.Logger);

                // Remove the already used challenge if it was validated. Otherwise keep it
                // for debugging purposes.
                if (challengeValidated && File.Exists(challengeFullPath))
                {
                    File.Delete(challengeFullPath);
                }

                // In the end, we always have a certificate. Program a renewal date according to the remaining expiration.
                currentCertificate       = UtilsIis.FindCertificateInCentralCertificateStore(hostName, this.Logger, out _);
                remainingCertificateDays = (currentCertificate?.NotAfter - DateTime.Now)?.TotalDays ?? 0;

                // Add some randomness in renewal dates to avoid all certificates being renewed at once and reaching api limits
                renewalState.LastRenewal = DateTime.UtcNow;
                renewalState.NextRenewal = this.CalculateNextRenewalAttempt(hostName, (int)remainingCertificateDays);
                this.StoreCertificateRenewalState(renewalState);
            }
            finally
            {
                this.Logger.LogInfo(true, "Disposing temporary verification setup");

                provider?.Dispose();

                this.UtilsHosts.RemoveHostsMapping(tempSiteAppId);

                // Restore the original state of IIS!!!
                using (ServerManager sm = new ServerManager())
                {
                    var site = sm.Sites.Single(i => i.Id == tempSiteId);
                    UtilsIis.RemoveSite(site, sm, this.Logger);
                    UtilsIis.CommitChanges(sm);
                }

                // Give IIS some time to reconfigure itself and free resources.
                Thread.Sleep(1000);

                // Start the sites
                foreach (var site in haltedSites)
                {
                    // Add some retry logic here because bringing the original sites online is critical
                    UtilsSystem.RetryWhile(() => { this.AppPoolUtils.WebsiteAction(site.Name, AppPoolActionType.Start); }, (e) => true, 5000, this.Logger);
                }
            }
        }
예제 #11
0
 public void TestIisGetConfigIsolationPath()
 {
     Assert.NotNull(UtilsIis.GetConfigIsolationPath());
 }
예제 #12
0
 public void TestGetMaxWebConfigFileSizeInKb()
 {
     UtilsIis.GetMaxWebConfigFileSizeInKb();
 }
예제 #13
0
        public void DeleteHttpsBindings()
        {
            var logger = new TestLogsLogger(this, nameof(this.DeleteHttpsBindings));

            using (ServerManager sm = new ServerManager())
            {
                var site = sm.Sites.Add("site0", Path.GetTempPath(), 443);
                site.Bindings.Add("127.0.0.1:5879:site0", null, null, SslFlags.CentralCertStore | SslFlags.Sni);
                site.Bindings.Add("127.0.0.1:5879:site1", null, null, SslFlags.CentralCertStore | SslFlags.Sni);
                sm.CommitChanges();
            }

            Thread.Sleep(500);
            var certificates = UtilsIis.GetBoundCertificates();

            Assert.True(certificates.Any((i) =>
                                         i.Attributes.Any((j) => j.Key == "Central Certificate Store" && j.Value == "5879")));

            using (ServerManager sm = new ServerManager())
            {
                var site = UtilsIis.FindSiteWithName(sm, "site0", logger).Single();
                UtilsIis.RemoveSiteBindings(site, sm, (i) => i.Host == "site1", logger);
                sm.CommitChanges();
            }

            certificates = UtilsIis.GetBoundCertificates();
            Assert.True(certificates.Any((i) =>
                                         i.Attributes.Any((j) => j.Key == "Central Certificate Store" && j.Value == "5879")));

            using (ServerManager sm = new ServerManager())
            {
                var site = UtilsIis.FindSiteWithName(sm, "site0", logger).Single();
                UtilsIis.RemoveSiteBindings(site, sm, (i) => i.Host == "site0", logger);
                sm.CommitChanges();
            }

            Thread.Sleep(500);
            certificates = UtilsIis.GetBoundCertificates();
            Assert.False(certificates.Any((i) =>
                                          i.Attributes.Any((j) => j.Key == "Central Certificate Store" && j.Value == "5879")));

            using (ServerManager sm = new ServerManager())
            {
                var site = UtilsIis.FindSiteWithName(sm, "site0", logger).Single();
                site.Bindings.Add("127.0.0.1:5879:site0", null, null, SslFlags.CentralCertStore | SslFlags.Sni);
                site.Bindings.Add("127.0.0.1:5879:site1", null, null, SslFlags.CentralCertStore | SslFlags.Sni);
                sm.CommitChanges();
            }

            Thread.Sleep(500);
            certificates = UtilsIis.GetBoundCertificates();
            Assert.True(certificates.Any((i) =>
                                         i.Attributes.Any((j) => j.Key == "Central Certificate Store" && j.Value == "5879")));

            using (ServerManager sm = new ServerManager())
            {
                var site = UtilsIis.FindSiteWithName(sm, "site0", logger).Single();
                UtilsIis.RemoveSite(site, sm, logger);
                sm.CommitChanges();
            }

            Thread.Sleep(500);
            certificates = UtilsIis.GetBoundCertificates();
            Assert.False(certificates.Any((i) =>
                                          i.Attributes.Any((j) => j.Key == "Central Certificate Store" && j.Value == "5879")));
        }
예제 #14
0
        public void ReadBoundCertificates()
        {
            var certificates = UtilsIis.GetBoundCertificates();

            Assert.NotEmpty(certificates);
        }
예제 #15
0
        /// <summary>
        /// Deploy fast-cgi settings
        /// </summary>
        protected void DeployFasctCgi()
        {
            var limits = this.Deployment.GetApplicationLimits();

            using (ServerManager serverManager = new ServerManager())
            {
                this.Logger.LogWarning(false, "Deploying fastCgi based applications causes IIS to internally reset to pick the new configuration because fastCgi is configured at the server level.");

                var phpRuntime  = this.GetFastCgiExe();
                var iniFilePath = this.GetIniFilePath();
                var phpIniFile  = this.GetIniFilePath();

                string siteAlias = this.Deployment.getShortId();

                // fastCgi settings in IIS can only be set at the HOSTS level
                // we found no way to set this at a web.config level.
                Microsoft.Web.Administration.Configuration config = serverManager.GetApplicationHostConfiguration();
                ConfigurationSection section = config.GetSection("system.webServer/fastCgi");
                ConfigurationElement cfs     = null;

                // Each fastCgi in IIS is a unique combination of RUNTIME_PATH|ARGUMENTS, try to find
                // the current application.
                foreach (ConfigurationElement sec in section.GetCollection())
                {
                    // Cada aplicación se identifica de manera única por la combincación de atributo y path de ejecución.
                    if (sec.HasValue("arguments", siteAlias) && sec.HasValue("fullPath", phpRuntime))
                    {
                        cfs = sec;
                        break;
                    }
                }

                // We need to keep track if the element already existed
                // in the configuration, or it is new.
                bool addApplication = false;
                ConfigurationElementCollection elems = section.GetCollection();
                if (cfs == null)
                {
                    cfs            = elems.CreateElement("application");
                    addApplication = true;
                }

                // In this deployment we are not really passing
                // any argments to PHP, simply use the site Alias to
                // isolate each PHP site.
                // OJO: PONER EL SITE ALIAS AQUÍ NO ES ALGO
                // GRATUITO. LUEGO EN EL WEB.CONFIG DE LA PROPIA
                // APLICACIÓN DEBE ESTAR EXACTAMENTE IGUAL.
                cfs.SetAttributeValue("arguments", siteAlias);

                // Set reasonable defaults, even if the user configuration says differently
                var instanceMaxRequests = this.PhpSettings.instanceMaxRequests > 200 ? this.PhpSettings.instanceMaxRequests : 10000;
                var maxInstances        = this.PhpSettings.maxInstances > 3 ? this.PhpSettings.maxInstances : 10;
                var activityTimeout     = this.PhpSettings.activityTimeout > 100 ? this.PhpSettings.activityTimeout : 600;
                var requestTimeout      = this.PhpSettings.requestTimeout > 60 ? this.PhpSettings.requestTimeout : 300;

                // Ensure that all values are within the limits
                if (maxInstances > limits.FastCgiMaxInstances && limits.FastCgiMaxInstances > 0)
                {
                    maxInstances = limits.FastCgiMaxInstances.Value;
                }

                // Runtime Path.
                cfs.SetAttributeValue("fullPath", phpRuntime);
                cfs.SetAttributeValue("maxInstances", maxInstances);
                cfs.SetAttributeValue("activityTimeout", activityTimeout);
                cfs.SetAttributeValue("requestTimeout", requestTimeout);
                cfs.SetAttributeValue("instanceMaxRequests", instanceMaxRequests);

                // Make sure that changes to PHP.ini are refreshed properly
                if (File.Exists(iniFilePath))
                {
                    cfs.SetAttributeValue("monitorChangesTo", iniFilePath);
                }

                // Este setting no sirve para nada según -E- de MS porque
                // la implementación de FastCGI está mal hecha en IIS.
                // Los eventos internos de señal no se llegan a ejecutar nunca,
                // lo único que consigues es demorar el cierre de instancias.
                cfs.SetAttributeValue("signalBeforeTerminateSeconds", 0);

                if (!File.Exists(phpIniFile))
                {
                    throw new Exception("PHP.ini file not found. This will break the IIS FastCgiModule when using monitorChangesTo feature.");
                }

                // Retrieve the environment variables.
                ConfigurationElement           cfgEnvironment = cfs.GetChildElement("environmentVariables");
                ConfigurationElementCollection a = cfgEnvironment.GetCollection();

                // This is fastcgi specific.
                a.AddOrUpdateConfigurationElementInCollection("PHP_FCGI_MAX_REQUESTS", instanceMaxRequests.ToString());

                // Add all the environment variables.
                var environmentVariables = this.GetEnvironmentVariables();
                foreach (var p in environmentVariables)
                {
                    a.AddOrUpdateConfigurationElementInCollection(p.Key, p.Value);
                }

                if (addApplication)
                {
                    elems.Add(cfs);
                }

                // Cleanup any fastCgi applications that point to non-existent handlers
                // see the comments in FastCgiRemove() as to why this is here.
                var fastCgiHandlers = section.GetCollection();
                foreach (ConfigurationElement sec in fastCgiHandlers.ToList())
                {
                    if (sec.RawAttributes.Keys.Contains("fullPath"))
                    {
                        string fullPath = sec.GetAttributeValue("fullPath").ToString();
                        if (!File.Exists(fullPath))
                        {
                            this.Logger.LogInfo(true, "Removed stale fastCgi handler {0}", fullPath);
                            fastCgiHandlers.Remove(sec);
                        }
                    }
                }

                UtilsIis.CommitChanges(serverManager);
            }
        }