using ACMESharp.Authorizations; using PKISharp.WACS.DomainObjects; using PKISharp.WACS.Plugins.Interfaces; using PKISharp.WACS.Services; using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; namespace PKISharp.WACS.Plugins.ValidationPlugins { /// /// Base implementation for HTTP-01 validation plugins /// internal abstract class HttpValidation : Validation where TOptions : HttpValidationOptions where TPlugin : IValidationPlugin { private bool _webConfigWritten = false; private bool _challengeWritten = false; protected TOptions _options; protected ILogService _log; protected IInputService _input; protected ISettingsService _settings; protected Renewal _renewal; protected RunLevel _runLevel; /// /// Path used for the current renewal, may not be same as _options.Path /// because of the "Split" function employed by IISSites target /// protected string _path; /// /// Provides proxy settings for site warmup /// private readonly ProxyService _proxy; /// /// Current TargetPart that we are working on. A TargetPart is mainly used by /// the IISSites TargetPlugin to indicate that we are working with different /// IIS sites /// protected TargetPart _targetPart; /// /// Where to find the template for the web.config that's copied to the webroot /// protected readonly string _templateWebConfig = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "web_config.xml"); /// /// Character to seperate folders, different for FTP /// protected virtual char PathSeparator => '\\'; /// /// Constructor /// /// /// /// /// /// /// /// /// public HttpValidation(TOptions options, RunLevel runLevel, HttpValidationParameters pars) { _options = options; _runLevel = runLevel; _path = options.Path; _log = pars.LogService; _input = pars.InputService; _proxy = pars.ProxyService; _settings = pars.Settings; _renewal = pars.Renewal; _targetPart = pars.TargetPart; } /// /// Handle http challenge /// public override void PrepareChallenge() { Refresh(); WriteAuthorizationFile(); WriteWebConfig(); _log.Information("Answer should now be browsable at {answerUri}", _challenge.HttpResourceUrl); if (_runLevel.HasFlag(RunLevel.Test) && _renewal.New) { if (_input.PromptYesNo("[--test] Try in default browser?", false)) { Process.Start(_challenge.HttpResourceUrl); _input.Wait(); } } string foundValue = null; try { var value = WarmupSite(); if (Equals(value, _challenge.HttpResourceValue)) { _log.Information("Preliminary validation looks good, but ACME will be more thorough..."); } else { _log.Warning("Preliminary validation failed, found {value} instead of {expected}", foundValue ?? "(null)", _challenge.HttpResourceValue); } } catch (Exception ex) { _log.Error(ex, "Preliminary validation failed"); } } /// /// Warm up the target site, giving the application a little /// time to start up before the validation request comes in. /// Mostly relevant to classic FileSystem validation /// /// private string WarmupSite() { var uri = new Uri(_challenge.HttpResourceUrl); var request = WebRequest.Create(uri); request.Proxy = _proxy.GetWebProxy(); using (var response = request.GetResponse()) { var responseStream = response.GetResponseStream(); using (var responseReader = new StreamReader(responseStream)) { return responseReader.ReadToEnd(); } } } /// /// Should create any directory structure needed and write the file for authorization /// /// where the answerFile should be located /// the contents of the file to write private void WriteAuthorizationFile() { WriteFile(CombinePath(_path, _challenge.HttpResourcePath), _challenge.HttpResourceValue); _challengeWritten = true; } /// /// Can be used to write out server specific configuration, to handle extensionless files etc. /// /// /// /// private void WriteWebConfig() { if (_options.CopyWebConfig == true) { _log.Debug("Writing web.config"); var partialPath = _challenge.HttpResourcePath.Split('/').Last(); var destination = CombinePath(_path, _challenge.HttpResourcePath.Replace(partialPath, "web.config")); var content = GetWebConfig(); WriteFile(destination, content); _webConfigWritten = true; } } /// /// Get the template for the web.config /// /// private string GetWebConfig() { return File.ReadAllText(_templateWebConfig); } /// /// Can be used to write out server specific configuration, to handle extensionless files etc. /// /// /// /// private void DeleteWebConfig() { if (_webConfigWritten) { _log.Debug("Deleting web.config"); var partialPath = _challenge.HttpResourcePath.Split('/').Last(); var destination = CombinePath(_path, _challenge.HttpResourcePath.Replace(partialPath, "web.config")); DeleteFile(destination); } } /// /// Should delete any authorizations /// /// where the answerFile should be located /// the token /// the website root path /// the file path for the authorization file private void DeleteAuthorization() { try { if (_challengeWritten) { _log.Debug("Deleting answer"); var path = CombinePath(_path, _challenge.HttpResourcePath); var partialPath = _challenge.HttpResourcePath.Split('/').Last(); DeleteFile(path); if (_settings.CleanupFolders) { path = path.Replace($"{PathSeparator}{partialPath}", ""); if (DeleteFolderIfEmpty(path)) { var idx = path.LastIndexOf(PathSeparator); if (idx >= 0) { path = path.Substring(0, path.LastIndexOf(PathSeparator)); DeleteFolderIfEmpty(path); } } } } } catch (Exception ex) { _log.Warning("Error occured while deleting folder structure. Error: {@ex}", ex); } } /// /// Combine root path with relative path /// /// /// /// protected virtual string CombinePath(string root, string path) { if (root == null) { root = string.Empty; } var expandedRoot = Environment.ExpandEnvironmentVariables(root); var trim = new[] { '/', '\\' }; return $"{expandedRoot.TrimEnd(trim)}{PathSeparator}{path.TrimStart(trim).Replace('/', PathSeparator)}"; } /// /// Delete folder if it's empty /// /// /// private bool DeleteFolderIfEmpty(string path) { if (IsEmpty(path)) { DeleteFolder(path); return true; } else { _log.Debug("Additional files or folders exist in {folder}, not deleting.", path); return false; } } /// /// Write file with content to a specific location /// /// /// /// protected abstract void WriteFile(string path, string content); /// /// Delete file from specific location /// /// /// protected abstract void DeleteFile(string path); /// /// Check if folder is empty /// /// /// protected abstract bool IsEmpty(string path); /// /// Delete folder if not empty /// /// /// protected abstract void DeleteFolder(string path); /// /// Refresh /// /// /// protected virtual void Refresh() { } /// /// Dispose /// public override void CleanUp() { DeleteWebConfig(); DeleteAuthorization(); } } }