/// <summary> /// Simulates responding to a challenge, performs a sample configuration and attempts to /// verify it. /// </summary> /// <param name="iisManager"></param> /// <param name="managedSite"></param> /// <returns> APIResult </returns> /// <remarks> /// The purpose of this method is to test the options (permissions, configuration) before /// submitting a request to the ACME server, to avoid creating failed requests and hitting /// usage limits. /// </remarks> public async Task <APIResult> TestChallengeResponse(IISManager iisManager, ManagedSite managedSite, bool isPreviewMode) { return(await Task.Run(() => { ActionLogs.Clear(); // reset action logs var requestConfig = managedSite.RequestConfig; var result = new APIResult(); var domains = new List <string> { requestConfig.PrimaryDomain }; if (requestConfig.SubjectAlternativeNames != null) { domains.AddRange(requestConfig.SubjectAlternativeNames); } var generatedAuthorizations = new List <PendingAuthorization>(); try { // if DNS checks enabled, attempt them here if (isPreviewMode && CoreAppSettings.Current.EnableDNSValidationChecks) { // check all domain configs Parallel.ForEach(domains.Distinct(), new ParallelOptions { // check 8 domains at a time MaxDegreeOfParallelism = 8 }, domain => { var(ok, message) = NetUtil.CheckDNS(domain); if (!ok) { result.IsOK = false; result.FailedItemSummary.Add(message); } }); if (!result.IsOK) { return result; } } if (requestConfig.ChallengeType == ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_HTTP) { foreach (var domain in domains.Distinct()) { string challengeFileUrl = $"http://{domain}/.well-known/acme-challenge/configcheck"; var simulatedAuthorization = new PendingAuthorization { Challenge = new AuthorizeChallengeItem { ChallengeData = new ACMESharp.ACME.HttpChallenge(ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_HTTP, new ACMESharp.ACME.HttpChallengeAnswer { KeyAuthorization = GenerateSimulatedKeyAuth() }) { FilePath = ".well-known/acme-challenge/configcheck", FileContent = "Extensionless File Config Test - OK", FileUrl = challengeFileUrl } } }; generatedAuthorizations.Add(simulatedAuthorization); var resultOK = PrepareChallengeResponse_Http01( iisManager, domain, managedSite, simulatedAuthorization )(); if (!resultOK) { result.IsOK = false; result.FailedItemSummary.Add($"Config checks failed to verify http://{domain} is both publicly accessible and can serve extensionless files e.g. {challengeFileUrl}"); } } } else if (requestConfig.ChallengeType == ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_SNI) { if (iisManager.GetIisVersion().Major < 8) { result.IsOK = false; result.FailedItemSummary.Add($"The {ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_SNI} challenge is only available for IIS versions 8+."); return result; } result.IsOK = domains.Distinct().All(domain => { var simulatedAuthorization = new PendingAuthorization { Challenge = new AuthorizeChallengeItem() { ChallengeData = new ACMESharp.ACME.TlsSniChallenge(ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_SNI, new ACMESharp.ACME.TlsSniChallengeAnswer { KeyAuthorization = GenerateSimulatedKeyAuth() }) { IterationCount = 1 } } }; generatedAuthorizations.Add(simulatedAuthorization); return PrepareChallengeResponse_TlsSni01( iisManager, domain, managedSite, simulatedAuthorization )(); }); } else { throw new NotSupportedException($"ChallengeType not supported: {requestConfig.ChallengeType}"); } } finally { //FIXME: needs to be filtered by managed site: result.Message = String.Join("\r\n", GetActionLogSummary()); generatedAuthorizations.ForEach(ga => ga.Cleanup()); } return result; })); }