Ejemplo n.º 1
0
        /// <summary>
        /// Prepares IIS to respond to a http-01 challenge
        /// </summary>
        /// <returns> Test the challenge response locally. </returns>
        private async Task <ActionResult> PerformChallengeResponse_Http01(ILog log, ICertifiedServer iisManager, string domain, ManagedCertificate managedCertificate, PendingAuthorization pendingAuth)
        {
            var requestConfig = managedCertificate.RequestConfig;
            var httpChallenge = pendingAuth.Challenges.FirstOrDefault(c => c.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP);

            if (httpChallenge == null)
            {
                var msg = $"No http challenge to complete for {managedCertificate.Name}. Request cannot continue.";
                log.Warning(msg);
                return(new ActionResult {
                    IsSuccess = false, Message = msg
                });
            }

            log.Information($"Preparing challenge response for the issuing Certificate Authority to check at: {httpChallenge.ResourceUri} with content {httpChallenge.Value}");
            log.Information("If the challenge response file is not accessible at this exact URL the validation will fail and a certificate will not be issued.");

            // get website root path (from challenge config or fallback to deprecated
            // WebsiteRootPath), expand environment variables if required
            var websiteRootPath = requestConfig.WebsiteRootPath;
            var challengeConfig = managedCertificate.GetChallengeConfig(domain);

            if (!string.IsNullOrEmpty(challengeConfig.ChallengeRootPath))
            {
                websiteRootPath = challengeConfig.ChallengeRootPath;
            }

            if (!string.IsNullOrEmpty(managedCertificate.ServerSiteId))
            {
                var siteInfo = await iisManager.GetSiteById(managedCertificate.ServerSiteId);

                if (siteInfo == null)
                {
                    return(new ActionResult {
                        IsSuccess = false, Message = "IIS Website unavailable. Site may be removed or IIS is unavailable"
                    });
                }

                // if website root path not specified, determine it now
                if (string.IsNullOrEmpty(websiteRootPath))
                {
                    websiteRootPath = siteInfo.Path;
                }

                if (!string.IsNullOrEmpty(websiteRootPath) && websiteRootPath.Contains("%"))
                {
                    // if websiteRootPath contains %websiteroot% variable, replace that with the
                    // current physical path for the site
                    if (websiteRootPath.Contains("%websiteroot%"))
                    {
                        // sets env variable for this process only
                        Environment.SetEnvironmentVariable("websiteroot", siteInfo.Path);
                    }
                    // expand any environment variables present in site path
                    websiteRootPath = Environment.ExpandEnvironmentVariables(websiteRootPath);
                }
            }

            log.Information("Using website path {path}", websiteRootPath ?? "[Auto]");

            if (string.IsNullOrEmpty(websiteRootPath) || !Directory.Exists(websiteRootPath))
            {
                // our website no longer appears to exist on disk, continuing would potentially
                // create unwanted folders, so it's time for us to give up

                var msg = $"The website root path for {managedCertificate.Name} could not be determined. Request cannot continue.";
                log.Error(msg);
                return(new ActionResult {
                    IsSuccess = false, Message = msg
                });
            }

            // copy temp file to path challenge expects in web folder
            var destFile = Path.Combine(websiteRootPath, httpChallenge.ResourcePath);
            var destPath = Path.GetDirectoryName(destFile);

            if (!Directory.Exists(destPath))
            {
                try
                {
                    Directory.CreateDirectory(destPath);
                }
                catch (Exception exp)
                {
                    // failed to create directory, probably permissions or may be invalid config

                    var msg = $"Pre-config check failed: Could not create directory: {destPath}";
                    log.Error(exp, msg);
                    return(new ActionResult {
                        IsSuccess = false, Message = msg
                    });
                }
            }

            // copy challenge response to web folder /.well-known/acme-challenge. Check if it already
            // exists (as in 'configcheck' file) as can cause conflicts.
            if (!File.Exists(destFile) || !destFile.EndsWith("configcheck"))
            {
                try
                {
                    File.WriteAllText(destFile, httpChallenge.Value);
                }
                catch (Exception exp)
                {
                    // failed to create configcheck file, probably permissions or may be invalid config

                    var msg = $"Pre-config check failed: Could not create file: {destFile}";
                    log.Error(exp, msg);
                    return(new ActionResult {
                        IsSuccess = false, Message = msg
                    });
                }
            }

            // prepare cleanup - should this be configurable? Because in some case many sites
            // renewing may all point to the same web root, we keep the configcheck file
            pendingAuth.Cleanup = () =>
            {
                if (!destFile.EndsWith("configcheck") && File.Exists(destFile))
                {
                    log.Debug("Challenge Cleanup: Removing {file}", destFile);
                    try
                    {
                        File.Delete(destFile);
                    }
                    catch { }
                }
            };

            // if config checks are enabled but our last renewal was successful, skip auto config
            // until we have failed twice
            if (requestConfig.PerformExtensionlessConfigChecks)
            {
                if (managedCertificate.DateRenewed != null && managedCertificate.RenewalFailureCount < 2)
                {
                    return(new ActionResult {
                        IsSuccess = true, Message = $"Skipping URL access checks and auto config (if applicable): {httpChallenge.ResourceUri}. Will resume checks if renewal failure count exceeds 2 attempts."
                    });
                }

                // first check if it already works with no changes
                if (await _netUtil.CheckURL(log, httpChallenge.ResourceUri))
                {
                    return(new ActionResult {
                        IsSuccess = true, Message = $"Verified URL is accessible: {httpChallenge.ResourceUri}"
                    });
                }

                // initial check didn't work, if auto config enabled attempt to find a working config
                if (requestConfig.PerformAutoConfig)
                {
                    // FIXME: need to only overwrite config we have auto populated, not user
                    // specified config, compare to our preconfig and only overwrite if same as ours?
                    // Or include preset key in our config, or make behaviour configurable
                    LogAction($"Pre-config check failed: Auto-config will overwrite existing config: {destPath}\\web.config");

                    var configOptions = Directory.EnumerateFiles(Path.Combine(Environment.CurrentDirectory, "Scripts", "Web.config"), "*.config");

                    foreach (var configFile in configOptions)
                    {
                        // create a web.config for extensionless files, then test it (make a request
                        // for the extensionless configcheck file over http)

                        var webConfigContent = File.ReadAllText(configFile);

                        // no existing config, attempt auto config and perform test
                        LogAction($"Testing config alternative: " + configFile);

                        try
                        {
                            System.IO.File.WriteAllText(Path.Combine(destPath, "web.config"), webConfigContent);
                        }
                        catch (Exception exp)
                        {
                            LogAction($"Failed to write config: " + exp.Message);
                        }

                        if (await _netUtil.CheckURL(log, httpChallenge.ResourceUri))
                        {
                            return(new ActionResult {
                                IsSuccess = true, Message = $"Verified URL is accessible: {httpChallenge.ResourceUri}"
                            });
                        }
                    }
                }

                // failed to auto configure or confirm resource is accessible
                return(new ActionResult
                {
                    IsSuccess = false,
                    Message = $"Could not verify URL is accessible: {httpChallenge.ResourceUri}"
                });
            }
            else
            {
                return(new ActionResult
                {
                    IsSuccess = false,
                    Message = $"Config checks disabled. Did not verify URL access: {httpChallenge.ResourceUri}"
                });
            }
        }
Ejemplo n.º 2
0
        /// <summary>
        /// Prepares IIS to respond to a http-01 challenge
        /// </summary>
        /// <returns>
        /// A Boolean returning Func. Invoke the Func to test the challenge response locally.
        /// </returns>
        private Func <bool> PrepareChallengeResponse_Http01(IISManager iisManager, string domain, ManagedSite managedSite, PendingAuthorization pendingAuth)
        {
            var requestConfig = managedSite.RequestConfig;
            var httpChallenge = (ACMESharp.ACME.HttpChallenge)pendingAuth.Challenge.ChallengeData;

            this.LogAction("Preparing challenge response for LetsEncrypt server to check at: " + httpChallenge.FileUrl);
            this.LogAction("If the challenge response file is not accessible at this exact URL the validation will fail and a certificate will not be issued.");

            // get website root path, expand environment variables if required
            string websiteRootPath = requestConfig.WebsiteRootPath;

            // if website root path not specified, determine it now
            if (String.IsNullOrEmpty(websiteRootPath))
            {
                websiteRootPath = iisManager.GetSitePhysicalPath(managedSite);
            }

            if (!String.IsNullOrEmpty(websiteRootPath) && websiteRootPath.Contains("%"))
            {
                // if websiteRootPath contains %websiteroot% variable, replace that with the current
                // physical path for the site
                if (websiteRootPath.Contains("%websiteroot%"))
                {
                    // sets env variable for this process only
                    Environment.SetEnvironmentVariable("websiteroot", iisManager.GetSitePhysicalPath(managedSite));
                }
                // expand any environment variables present in site path
                websiteRootPath = Environment.ExpandEnvironmentVariables(websiteRootPath);
            }

            if (String.IsNullOrEmpty(websiteRootPath) || !Directory.Exists(websiteRootPath))
            {
                // our website no longer appears to exist on disk, continuing would potentially
                // create unwanted folders, so it's time for us to give up
                this.LogAction($"The website root path for {managedSite.Name} could not be determined. Request cannot continue.");
                return(() => false);
            }

            // copy temp file to path challenge expects in web folder
            var destFile = Path.Combine(websiteRootPath, httpChallenge.FilePath);
            var destPath = Path.GetDirectoryName(destFile);

            if (!Directory.Exists(destPath))
            {
                Directory.CreateDirectory(destPath);
            }

            // copy challenge response to web folder /.well-known/acme-challenge. Check if it already
            // exists (as in 'configcheck' file) as can cause conflicts.
            if (!File.Exists(destFile))
            {
                File.WriteAllText(destFile, httpChallenge.FileContent);
            }

            // configure cleanup - should this be configurable? Because in some case many sites
            // renewing may all point to the same web root, we keep the configcheck file
            pendingAuth.Cleanup = () =>
            {
                if (!destFile.EndsWith("configcheck"))
                {
                    File.Delete(destFile);
                }
            };

            // create a web.config for extensionless files, then test it (make a request for the
            // extensionless configcheck file over http)
            string webConfigContent = Core.Properties.Resources.IISWebConfig;

            if (!File.Exists(destPath + "\\web.config"))
            {
                // no existing config, attempt auto config and perform test
                this.LogAction($"Config does not exist, writing default config to: {destPath}\\web.config");
                System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent);
                return(() => NetUtil.CheckURL($"http://{domain}/{httpChallenge.FilePath}"));
            }
            else
            {
                // web config already exists, don't overwrite it, just test it
                return(() =>
                {
                    if (NetUtil.CheckURL($"http://{domain}/{httpChallenge.FilePath}"))
                    {
                        return true;
                    }

                    if (requestConfig.PerformAutoConfig)
                    {
                        this.LogAction($"Pre-config check failed: Auto-config will overwrite existing config: {destPath}\\web.config");
                        // didn't work, try our default config
                        System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent);

                        if (NetUtil.CheckURL($"http://{domain}/{httpChallenge.FilePath}"))
                        {
                            return true;
                        }
                    }
                    return false;
                });
            }
        }