summaryrefslogtreecommitdiffstats
path: root/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs')
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs676
1 files changed, 338 insertions, 338 deletions
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs
index ee6a996..a45e9e2 100644
--- a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs
@@ -1,338 +1,338 @@
-using ACMESharp.Authorizations;
-using PKISharp.WACS.Context;
-using PKISharp.WACS.DomainObjects;
-using PKISharp.WACS.Plugins.Interfaces;
-using PKISharp.WACS.Services;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-namespace PKISharp.WACS.Plugins.ValidationPlugins
-{
- /// <summary>
- /// Base implementation for HTTP-01 validation plugins
- /// </summary>
- internal abstract class HttpValidation<TOptions, TPlugin> :
- Validation<Http01ChallengeValidationDetails>
- where TOptions : HttpValidationOptions<TPlugin>
- where TPlugin : IValidationPlugin
- {
- private readonly List<string> _filesWritten = new List<string>();
-
- protected TOptions _options;
- protected ILogService _log;
- protected IInputService _input;
- protected ISettingsService _settings;
- protected Renewal _renewal;
- protected RunLevel _runLevel;
-
- /// <summary>
- /// Multiple http-01 validation challenges can be answered at the same time
- /// </summary>
- public override ParallelOperations Parallelism => ParallelOperations.Answer;
-
- /// <summary>
- /// Path used for the current renewal, may not be same as _options.Path
- /// because of the "Split" function employed by IISSites target
- /// </summary>
- protected string? _path;
-
- /// <summary>
- /// Provides proxy settings for site warmup
- /// </summary>
- private readonly ProxyService _proxy;
-
- /// <summary>
- /// Where to find the template for the web.config that's copied to the webroot
- /// </summary>
- protected string TemplateWebConfig => Path.Combine(Path.GetDirectoryName(_settings.ExePath), "web_config.xml");
-
- /// <summary>
- /// Character to seperate folders, different for FTP
- /// </summary>
- protected virtual char PathSeparator => '\\';
-
- /// <summary>
- /// Constructor
- /// </summary>
- /// <param name="log"></param>
- /// <param name="input"></param>
- /// <param name="options"></param>
- /// <param name="proxy"></param>
- /// <param name="renewal"></param>
- /// <param name="target"></param>
- /// <param name="runLevel"></param>
- /// <param name="identifier"></param>
- 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;
- }
-
- /// <summary>
- /// Handle http challenge
- /// </summary>
- public async override Task PrepareChallenge(ValidationContext context, Http01ChallengeValidationDetails challenge)
- {
- // Should always have a value, confirmed by RenewalExecutor
- // check only to satifiy the compiler
- if (context.TargetPart != null)
- {
- Refresh(context.TargetPart);
- }
- WriteAuthorizationFile(challenge);
- WriteWebConfig(challenge);
- _log.Information("Answer should now be browsable at {answerUri}", challenge.HttpResourceUrl);
- if (_runLevel.HasFlag(RunLevel.Test) && _renewal.New)
- {
- if (await _input.PromptYesNo("[--test] Try in default browser?", false))
- {
- Process.Start(new ProcessStartInfo
- {
- FileName = challenge.HttpResourceUrl,
- UseShellExecute = true
- });
- await _input.Wait();
- }
- }
-
- string? foundValue = null;
- try
- {
- var value = await WarmupSite(challenge);
- if (Equals(value, challenge.HttpResourceValue))
- {
- _log.Information("Preliminary validation looks good, but the ACME server will be more thorough");
- }
- else
- {
- _log.Warning("Preliminary validation failed, the server answered '{value}' instead of '{expected}'. The ACME server might have a different perspective",
- foundValue ?? "(null)",
- challenge.HttpResourceValue);
- }
- }
- catch (HttpRequestException hrex)
- {
- _log.Warning("Preliminary validation failed because '{hrex}'", hrex.Message);
- }
- catch (Exception ex)
- {
- _log.Error(ex, "Preliminary validation failed");
- }
- }
-
- /// <summary>
- /// Default commit function, doesn't do anything because
- /// default doesn't do parallel operation
- /// </summary>
- /// <returns></returns>
- public override Task Commit() => Task.CompletedTask;
-
- /// <summary>
- /// 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
- /// </summary>
- /// <param name="uri"></param>
- private async Task<string> WarmupSite(Http01ChallengeValidationDetails challenge)
- {
- using var client = _proxy.GetHttpClient(false);
- var response = await client.GetAsync(challenge.HttpResourceUrl);
- return await response.Content.ReadAsStringAsync();
- }
-
- /// <summary>
- /// Should create any directory structure needed and write the file for authorization
- /// </summary>
- /// <param name="answerPath">where the answerFile should be located</param>
- /// <param name="fileContents">the contents of the file to write</param>
- private void WriteAuthorizationFile(Http01ChallengeValidationDetails challenge)
- {
- if (_path == null)
- {
- throw new InvalidOperationException();
- }
- var path = CombinePath(_path, challenge.HttpResourcePath);
- WriteFile(path, challenge.HttpResourceValue);
- if (!_filesWritten.Contains(path))
- {
- _filesWritten.Add(path);
- }
- }
-
- /// <summary>
- /// Can be used to write out server specific configuration, to handle extensionless files etc.
- /// </summary>
- /// <param name="target"></param>
- /// <param name="answerPath"></param>
- /// <param name="token"></param>
- private void WriteWebConfig(Http01ChallengeValidationDetails challenge)
- {
- if (_path == null)
- {
- throw new InvalidOperationException();
- }
- if (_options.CopyWebConfig == true)
- {
- try
- {
- var partialPath = challenge.HttpResourcePath.Split('/').Last();
- var destination = CombinePath(_path, challenge.HttpResourcePath.Replace(partialPath, "web.config"));
- if (!_filesWritten.Contains(destination))
- {
- var content = GetWebConfig().Value;
- if (content != null)
- {
- _log.Debug("Writing web.config");
- WriteFile(destination, content);
- _filesWritten.Add(destination);
- }
-
- }
- }
- catch (Exception ex)
- {
- _log.Warning("Unable to write web.config: {ex}", ex.Message); ;
- }
- }
- }
-
- /// <summary>
- /// Get the template for the web.config
- /// </summary>
- /// <returns></returns>
- private Lazy<string?> GetWebConfig() => new Lazy<string?>(() => {
- try
- {
- return File.ReadAllText(TemplateWebConfig);
- }
- catch
- {
- return null;
- }
- });
-
- /// <summary>
- /// Combine root path with relative path
- /// </summary>
- /// <param name="root"></param>
- /// <param name="path"></param>
- /// <returns></returns>
- 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)}";
- }
-
- /// <summary>
- /// Delete folder if it's empty
- /// </summary>
- /// <param name="path"></param>
- /// <returns></returns>
- private async Task<bool> DeleteFolderIfEmpty(string path)
- {
- if (await IsEmpty(path))
- {
- await DeleteFolder(path);
- return true;
- }
- else
- {
- _log.Debug("Additional files or folders exist in {folder}, not deleting.", path);
- return false;
- }
- }
-
- /// <summary>
- /// Write file with content to a specific location
- /// </summary>
- /// <param name="root"></param>
- /// <param name="path"></param>
- /// <param name="content"></param>
- protected abstract Task WriteFile(string path, string content);
-
- /// <summary>
- /// Delete file from specific location
- /// </summary>
- /// <param name="root"></param>
- /// <param name="path"></param>
- protected abstract Task DeleteFile(string path);
-
- /// <summary>
- /// Check if folder is empty
- /// </summary>
- /// <param name="root"></param>
- /// <param name="path"></param>
- protected abstract Task<bool> IsEmpty(string path);
-
- /// <summary>
- /// Delete folder if not empty
- /// </summary>
- /// <param name="root"></param>
- /// <param name="path"></param>
- protected abstract Task DeleteFolder(string path);
-
- /// <summary>
- /// Refresh
- /// </summary>
- /// <param name="scheduled"></param>
- /// <returns></returns>
- protected virtual void Refresh(TargetPart targetPart) { }
-
- /// <summary>
- /// Dispose
- /// </summary>
- public override async Task CleanUp()
- {
- try
- {
- if (_path != null)
- {
- var folders = new List<string>();
- foreach (var file in _filesWritten)
- {
- _log.Debug("Deleting files");
- await DeleteFile(file);
- var folder = file.Substring(0, file.LastIndexOf(PathSeparator));
- if (!folders.Contains(folder))
- {
- folders.Add(folder);
- }
- }
- if (_settings.Validation.CleanupFolders)
- {
- _log.Debug("Deleting empty folders");
- foreach (var folder in folders)
- {
- if (await DeleteFolderIfEmpty(folder))
- {
- var idx = folder.LastIndexOf(PathSeparator);
- if (idx >= 0)
- {
- var parent = folder.Substring(0, folder.LastIndexOf(PathSeparator));
- await DeleteFolderIfEmpty(parent);
- }
- }
- }
- }
- }
- }
- catch (Exception ex)
- {
- _log.Warning("Error occured while deleting folder structure. Error: {@ex}", ex);
- }
- }
- }
-}
+using ACMESharp.Authorizations;
+using PKISharp.WACS.Context;
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Plugins.Interfaces;
+using PKISharp.WACS.Services;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins
+{
+ /// <summary>
+ /// Base implementation for HTTP-01 validation plugins
+ /// </summary>
+ internal abstract class HttpValidation<TOptions, TPlugin> :
+ Validation<Http01ChallengeValidationDetails>
+ where TOptions : HttpValidationOptions<TPlugin>
+ where TPlugin : IValidationPlugin
+ {
+ private readonly List<string> _filesWritten = new List<string>();
+
+ protected TOptions _options;
+ protected ILogService _log;
+ protected IInputService _input;
+ protected ISettingsService _settings;
+ protected Renewal _renewal;
+ protected RunLevel _runLevel;
+
+ /// <summary>
+ /// Multiple http-01 validation challenges can be answered at the same time
+ /// </summary>
+ public override ParallelOperations Parallelism => ParallelOperations.Answer;
+
+ /// <summary>
+ /// Path used for the current renewal, may not be same as _options.Path
+ /// because of the "Split" function employed by IISSites target
+ /// </summary>
+ protected string? _path;
+
+ /// <summary>
+ /// Provides proxy settings for site warmup
+ /// </summary>
+ private readonly ProxyService _proxy;
+
+ /// <summary>
+ /// Where to find the template for the web.config that's copied to the webroot
+ /// </summary>
+ protected string TemplateWebConfig => Path.Combine(Path.GetDirectoryName(_settings.ExePath), "web_config.xml");
+
+ /// <summary>
+ /// Character to seperate folders, different for FTP
+ /// </summary>
+ protected virtual char PathSeparator => '\\';
+
+ /// <summary>
+ /// Constructor
+ /// </summary>
+ /// <param name="log"></param>
+ /// <param name="input"></param>
+ /// <param name="options"></param>
+ /// <param name="proxy"></param>
+ /// <param name="renewal"></param>
+ /// <param name="target"></param>
+ /// <param name="runLevel"></param>
+ /// <param name="identifier"></param>
+ 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;
+ }
+
+ /// <summary>
+ /// Handle http challenge
+ /// </summary>
+ public async override Task PrepareChallenge(ValidationContext context, Http01ChallengeValidationDetails challenge)
+ {
+ // Should always have a value, confirmed by RenewalExecutor
+ // check only to satifiy the compiler
+ if (context.TargetPart != null)
+ {
+ Refresh(context.TargetPart);
+ }
+ WriteAuthorizationFile(challenge);
+ WriteWebConfig(challenge);
+ _log.Information("Answer should now be browsable at {answerUri}", challenge.HttpResourceUrl);
+ if (_runLevel.HasFlag(RunLevel.Test) && _renewal.New)
+ {
+ if (await _input.PromptYesNo("[--test] Try in default browser?", false))
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = challenge.HttpResourceUrl,
+ UseShellExecute = true
+ });
+ await _input.Wait();
+ }
+ }
+
+ string? foundValue = null;
+ try
+ {
+ var value = await WarmupSite(challenge);
+ if (Equals(value, challenge.HttpResourceValue))
+ {
+ _log.Information("Preliminary validation looks good, but the ACME server will be more thorough");
+ }
+ else
+ {
+ _log.Warning("Preliminary validation failed, the server answered '{value}' instead of '{expected}'. The ACME server might have a different perspective",
+ foundValue ?? "(null)",
+ challenge.HttpResourceValue);
+ }
+ }
+ catch (HttpRequestException hrex)
+ {
+ _log.Warning("Preliminary validation failed because '{hrex}'", hrex.Message);
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Preliminary validation failed");
+ }
+ }
+
+ /// <summary>
+ /// Default commit function, doesn't do anything because
+ /// default doesn't do parallel operation
+ /// </summary>
+ /// <returns></returns>
+ public override Task Commit() => Task.CompletedTask;
+
+ /// <summary>
+ /// 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
+ /// </summary>
+ /// <param name="uri"></param>
+ private async Task<string> WarmupSite(Http01ChallengeValidationDetails challenge)
+ {
+ using var client = _proxy.GetHttpClient(false);
+ var response = await client.GetAsync(challenge.HttpResourceUrl);
+ return await response.Content.ReadAsStringAsync();
+ }
+
+ /// <summary>
+ /// Should create any directory structure needed and write the file for authorization
+ /// </summary>
+ /// <param name="answerPath">where the answerFile should be located</param>
+ /// <param name="fileContents">the contents of the file to write</param>
+ private void WriteAuthorizationFile(Http01ChallengeValidationDetails challenge)
+ {
+ if (_path == null)
+ {
+ throw new InvalidOperationException();
+ }
+ var path = CombinePath(_path, challenge.HttpResourcePath);
+ WriteFile(path, challenge.HttpResourceValue);
+ if (!_filesWritten.Contains(path))
+ {
+ _filesWritten.Add(path);
+ }
+ }
+
+ /// <summary>
+ /// Can be used to write out server specific configuration, to handle extensionless files etc.
+ /// </summary>
+ /// <param name="target"></param>
+ /// <param name="answerPath"></param>
+ /// <param name="token"></param>
+ private void WriteWebConfig(Http01ChallengeValidationDetails challenge)
+ {
+ if (_path == null)
+ {
+ throw new InvalidOperationException();
+ }
+ if (_options.CopyWebConfig == true)
+ {
+ try
+ {
+ var partialPath = challenge.HttpResourcePath.Split('/').Last();
+ var destination = CombinePath(_path, challenge.HttpResourcePath.Replace(partialPath, "web.config"));
+ if (!_filesWritten.Contains(destination))
+ {
+ var content = GetWebConfig().Value;
+ if (content != null)
+ {
+ _log.Debug("Writing web.config");
+ WriteFile(destination, content);
+ _filesWritten.Add(destination);
+ }
+
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Warning("Unable to write web.config: {ex}", ex.Message); ;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Get the template for the web.config
+ /// </summary>
+ /// <returns></returns>
+ private Lazy<string?> GetWebConfig() => new Lazy<string?>(() => {
+ try
+ {
+ return File.ReadAllText(TemplateWebConfig);
+ }
+ catch
+ {
+ return null;
+ }
+ });
+
+ /// <summary>
+ /// Combine root path with relative path
+ /// </summary>
+ /// <param name="root"></param>
+ /// <param name="path"></param>
+ /// <returns></returns>
+ 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)}";
+ }
+
+ /// <summary>
+ /// Delete folder if it's empty
+ /// </summary>
+ /// <param name="path"></param>
+ /// <returns></returns>
+ private async Task<bool> DeleteFolderIfEmpty(string path)
+ {
+ if (await IsEmpty(path))
+ {
+ await DeleteFolder(path);
+ return true;
+ }
+ else
+ {
+ _log.Debug("Not deleting {path} because it doesn't exist or it's not empty.", path);
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Write file with content to a specific location
+ /// </summary>
+ /// <param name="root"></param>
+ /// <param name="path"></param>
+ /// <param name="content"></param>
+ protected abstract Task WriteFile(string path, string content);
+
+ /// <summary>
+ /// Delete file from specific location
+ /// </summary>
+ /// <param name="root"></param>
+ /// <param name="path"></param>
+ protected abstract Task DeleteFile(string path);
+
+ /// <summary>
+ /// Check if folder is empty
+ /// </summary>
+ /// <param name="root"></param>
+ /// <param name="path"></param>
+ protected abstract Task<bool> IsEmpty(string path);
+
+ /// <summary>
+ /// Delete folder if not empty
+ /// </summary>
+ /// <param name="root"></param>
+ /// <param name="path"></param>
+ protected abstract Task DeleteFolder(string path);
+
+ /// <summary>
+ /// Refresh
+ /// </summary>
+ /// <param name="scheduled"></param>
+ /// <returns></returns>
+ protected virtual void Refresh(TargetPart targetPart) { }
+
+ /// <summary>
+ /// Dispose
+ /// </summary>
+ public override async Task CleanUp()
+ {
+ try
+ {
+ if (_path != null)
+ {
+ var folders = new List<string>();
+ foreach (var file in _filesWritten)
+ {
+ _log.Debug("Deleting files");
+ await DeleteFile(file);
+ var folder = file.Substring(0, file.LastIndexOf(PathSeparator));
+ if (!folders.Contains(folder))
+ {
+ folders.Add(folder);
+ }
+ }
+ if (_settings.Validation.CleanupFolders)
+ {
+ _log.Debug("Deleting empty folders");
+ foreach (var folder in folders)
+ {
+ if (await DeleteFolderIfEmpty(folder))
+ {
+ var idx = folder.LastIndexOf(PathSeparator);
+ if (idx >= 0)
+ {
+ var parent = folder.Substring(0, folder.LastIndexOf(PathSeparator));
+ await DeleteFolderIfEmpty(parent);
+ }
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Error occured while deleting folder structure");
+ }
+ }
+ }
+}