summaryrefslogtreecommitdiffstats
path: root/src/main.lib/Plugins/ValidationPlugins/Http
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.lib/Plugins/ValidationPlugins/Http')
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs87
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemArguments.cs7
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemArgumentsProvider.cs24
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemOptions.cs35
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemOptionsFactory.cs69
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs43
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/Ftp/FtpOptions.cs31
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/Ftp/FtpOptionsFactory.cs58
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs312
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationArguments.cs9
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationArgumentsProvider.cs33
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationOptions.cs32
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationOptionsFactory.cs93
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationParameters.cs37
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHosting.cs69
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingArguments.cs7
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingArgumentsProvider.cs25
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptions.cs33
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptionsFactory.cs30
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs44
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/Sftp/SftpOptions.cs31
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/Sftp/SftpOptionsFactory.cs41
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDav.cs46
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDavOptions.cs31
-rw-r--r--src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDavOptionsFactory.cs47
25 files changed, 1274 insertions, 0 deletions
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs
new file mode 100644
index 0000000..1301f5b
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystem.cs
@@ -0,0 +1,87 @@
+using PKISharp.WACS.Clients.IIS;
+using PKISharp.WACS.Extensions;
+using System;
+using System.IO;
+using System.Linq;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ internal class FileSystem : HttpValidation<FileSystemOptions, FileSystem>
+ {
+ protected IIISClient _iisClient;
+
+ public FileSystem(FileSystemOptions options, IIISClient iisClient, RunLevel runLevel, HttpValidationParameters pars) : base(options, runLevel, pars)
+ {
+ _iisClient = iisClient;
+ }
+
+ protected override void DeleteFile(string path)
+ {
+ var fi = new FileInfo(path);
+ if (fi.Exists)
+ {
+ _log.Verbose("Deleting file {path}", path);
+ fi.Delete();
+ }
+ else
+ {
+ _log.Warning("File {path} already deleted", path);
+ }
+ }
+
+ protected override void DeleteFolder(string path)
+ {
+ var di = new DirectoryInfo(path);
+ if (di.Exists)
+ {
+ _log.Verbose("Deleting folder {path}", path);
+ di.Delete();
+ }
+ else
+ {
+ _log.Warning("Folder {path} already deleted", path);
+ }
+ }
+
+ protected override bool IsEmpty(string path)
+ {
+ return !(new DirectoryInfo(path)).GetFileSystemInfos().Any();
+ }
+
+ protected override void WriteFile(string path, string content)
+ {
+ var fi = new FileInfo(path);
+ if (!fi.Directory.Exists)
+ {
+ fi.Directory.Create();
+ }
+ _log.Verbose("Writing file to {path}", path);
+ File.WriteAllText(path, content);
+ }
+
+ /// <summary>
+ /// Update webroot
+ /// </summary>
+ /// <param name="scheduled"></param>
+ protected override void Refresh()
+ {
+ if (string.IsNullOrEmpty(_options.Path))
+ {
+ // Update web root path
+ var siteId = _options.SiteId ?? _targetPart.SiteId;
+ if (siteId > 0)
+ {
+ _path = _iisClient.GetWebSite(siteId.Value).Path;
+ }
+ else
+ {
+ throw new Exception("No path specified");
+ }
+ }
+ else
+ {
+ _path = _options.Path;
+ }
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemArguments.cs b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemArguments.cs
new file mode 100644
index 0000000..b99f400
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemArguments.cs
@@ -0,0 +1,7 @@
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ class FileSystemArguments : HttpValidationArguments
+ {
+ public long? ValidationSiteId { get; set; }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemArgumentsProvider.cs b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemArgumentsProvider.cs
new file mode 100644
index 0000000..4d696e1
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemArgumentsProvider.cs
@@ -0,0 +1,24 @@
+using Fclp;
+using PKISharp.WACS.Configuration;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ class FileSystemArgumentsProvider : BaseArgumentsProvider<FileSystemArguments>
+ {
+ public override string Name => "FileSystem plugin";
+ public override string Condition => "--validation filesystem";
+ public override string Group => "Validation";
+
+ public override void Configure(FluentCommandLineParser<FileSystemArguments> parser)
+ {
+ parser.Setup(o => o.ValidationSiteId)
+ .As("validationsiteid")
+ .WithDescription("Specify IIS site to use for handling validation requests. This will be used to choose the web root path.");
+ }
+
+ public override bool Active(FileSystemArguments current)
+ {
+ return current.ValidationSiteId != null;
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemOptions.cs b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemOptions.cs
new file mode 100644
index 0000000..4b70e2d
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemOptions.cs
@@ -0,0 +1,35 @@
+using PKISharp.WACS.Plugins.Base;
+using PKISharp.WACS.Services;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ [Plugin("1c77b3a4-5310-4c46-92c6-00d866e84d6b")]
+ internal class FileSystemOptions : HttpValidationOptions<FileSystem>
+ {
+ public override string Name { get => "FileSystem"; }
+ public override string Description { get => "Save verification files on (network) path"; }
+
+ public FileSystemOptions() : base() { }
+ public FileSystemOptions(HttpValidationOptions<FileSystem> source) : base(source) { }
+
+ /// <summary>
+ /// Alternative site for validation. The path will be
+ /// determined from this site on each validation attempt
+ /// </summary>
+ public long? SiteId { get; set; }
+
+ /// <summary>
+ /// Show to use what has been configured
+ /// </summary>
+ /// <param name="input"></param>
+ public override void Show(IInputService input)
+ {
+ base.Show(input);
+ if (SiteId != null)
+ {
+ input.Show("Site", SiteId.ToString());
+ }
+ }
+
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemOptionsFactory.cs
new file mode 100644
index 0000000..ea639d5
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/FileSystem/FileSystemOptionsFactory.cs
@@ -0,0 +1,69 @@
+using Microsoft.Web.Administration;
+using PKISharp.WACS.Clients.IIS;
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Extensions;
+using PKISharp.WACS.Services;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ /// <summary>
+ /// Classic FileSystem validation
+ /// </summary>
+ internal class FileSystemOptionsFactory : HttpValidationOptionsFactory<FileSystem, FileSystemOptions>
+ {
+ private readonly IIISClient _iisClient;
+ private readonly ILogService _log;
+
+ public FileSystemOptionsFactory(
+ IIISClient iisClient, ILogService log,
+ IArgumentsService arguments) : base(arguments)
+ {
+ _log = log;
+ _iisClient = iisClient;
+ }
+
+ public override bool PathIsValid(string path) => path.ValidPath(_log);
+ public override bool AllowEmtpy(Target target) => target.IIS;
+
+ public override FileSystemOptions Default(Target target)
+ {
+ var args = _arguments.GetArguments<FileSystemArguments>();
+ var ret = new FileSystemOptions(BaseDefault(target));
+ if (target.IIS && _iisClient.HasWebSites)
+ {
+
+ if (args.ValidationSiteId != null)
+ {
+ // Throws exception when not found
+ var site = _iisClient.GetWebSite(args.ValidationSiteId.Value);
+ ret.Path = site.Path;
+ ret.SiteId = args.ValidationSiteId.Value;
+ }
+ }
+ return ret;
+ }
+
+ public override FileSystemOptions Aquire(Target target, IInputService inputService, RunLevel runLevel)
+ {
+ // Choose alternative site for validation
+ var ret = new FileSystemOptions(BaseAquire(target, inputService, runLevel));
+ if (target.IIS && _iisClient.HasWebSites && string.IsNullOrEmpty(ret.Path))
+ {
+ if (inputService.PromptYesNo("Use different site for validation?", false))
+ {
+ var site = inputService.ChooseFromList("Validation site, must receive requests for all hosts on port 80",
+ _iisClient.WebSites,
+ x => Choice.Create(x, x.Name, x.Id.ToString()),
+ "Automatic (target site)");
+ if (site != null)
+ {
+ ret.Path = site.Path;
+ ret.SiteId = site.Id;
+ }
+ }
+ }
+ return ret;
+ }
+ }
+
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs b/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs
new file mode 100644
index 0000000..8ad5ab7
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/Ftp.cs
@@ -0,0 +1,43 @@
+using PKISharp.WACS.Clients;
+using System.Linq;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ internal class Ftp : HttpValidation<FtpOptions, Ftp>
+ {
+ private FtpClient _ftpClient;
+
+ public Ftp(FtpOptions options, HttpValidationParameters pars, RunLevel runLevel) : base(options, runLevel, pars)
+ {
+ _ftpClient = new FtpClient(_options.Credential, pars.LogService);
+ }
+
+ protected override char PathSeparator => '/';
+
+ protected override void DeleteFile(string path)
+ {
+ _ftpClient.Delete(path, FtpClient.FileType.File);
+ }
+
+ protected override void DeleteFolder(string path)
+ {
+ _ftpClient.Delete(path, FtpClient.FileType.Directory);
+ }
+
+ protected override bool IsEmpty(string path)
+ {
+ return !_ftpClient.GetFiles(path).Any();
+ }
+
+ protected override void WriteFile(string path, string content)
+ {
+ _ftpClient.Upload(path, content);
+ }
+
+ public override void CleanUp()
+ {
+ base.CleanUp();
+ _ftpClient = null;
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/FtpOptions.cs b/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/FtpOptions.cs
new file mode 100644
index 0000000..889dbe1
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/FtpOptions.cs
@@ -0,0 +1,31 @@
+using PKISharp.WACS.Configuration;
+using PKISharp.WACS.Plugins.Base;
+using PKISharp.WACS.Services;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ [Plugin("bc27d719-dcf2-41ff-bf08-54db7ea49c48")]
+ internal class FtpOptions : HttpValidationOptions<Ftp>
+ {
+ public override string Name { get => "FTP"; }
+ public override string Description { get => "Upload verification files via FTP(S)"; }
+
+ public FtpOptions() : base() { }
+ public FtpOptions(HttpValidationOptions<Ftp> source) : base(source) { }
+
+ /// <summary>
+ /// Credentials to use for WebDav connection
+ /// </summary>
+ public NetworkCredentialOptions Credential { get; set; }
+
+ /// <summary>
+ /// Show settings to user
+ /// </summary>
+ /// <param name="input"></param>
+ public override void Show(IInputService input)
+ {
+ base.Show(input);
+ Credential.Show(input);
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/FtpOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/FtpOptionsFactory.cs
new file mode 100644
index 0000000..ef6897f
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/Ftp/FtpOptionsFactory.cs
@@ -0,0 +1,58 @@
+using PKISharp.WACS.Configuration;
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Services;
+using System;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ internal class FtpOptionsFactory : HttpValidationOptionsFactory<Ftp, FtpOptions>
+ {
+ private readonly ILogService _log;
+
+ public FtpOptionsFactory(
+ ILogService log, IArgumentsService arguments) :
+ base(arguments)
+ {
+ _log = log;
+ }
+
+ public override bool PathIsValid(string path)
+ {
+ try
+ {
+ var uri = new Uri(path);
+ return uri.Scheme == "ftp" || uri.Scheme == "ftps";
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Invalid path");
+ return false;
+ }
+ }
+
+ public override string[] WebrootHint(bool allowEmpty)
+ {
+ return new[] {
+ "Enter an ftp path that leads to the web root of the host for http authentication",
+ " Example, ftp://domain.com:21/site/wwwroot/",
+ " Example, ftps://domain.com:990/site/wwwroot/"
+ };
+ }
+
+ public override FtpOptions Default(Target target)
+ {
+ return new FtpOptions(BaseDefault(target))
+ {
+ Credential = new NetworkCredentialOptions(_arguments)
+ };
+ }
+
+ public override FtpOptions Aquire(Target target, IInputService inputService, RunLevel runLevel)
+ {
+ return new FtpOptions(BaseAquire(target, inputService, runLevel))
+ {
+ Credential = new NetworkCredentialOptions(_arguments, inputService)
+ };
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs
new file mode 100644
index 0000000..6c8dc64
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidation.cs
@@ -0,0 +1,312 @@
+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
+{
+ /// <summary>
+ /// Base implementation for HTTP-01 validation plugins
+ /// </summary>
+ internal abstract class HttpValidation<TOptions, TPlugin> :
+ Validation<TOptions, Http01ChallengeValidationDetails>
+ where TOptions : HttpValidationOptions<TPlugin>
+ where TPlugin : IValidationPlugin
+ {
+
+ private bool _webConfigWritten = false;
+ private bool _challengeWritten = false;
+
+ protected IInputService _input;
+ protected ISettingsService _settings;
+ protected Renewal _renewal;
+ protected RunLevel _runLevel;
+
+ /// <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>
+ /// 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
+ /// </summary>
+ protected TargetPart _targetPart;
+
+ /// <summary>
+ /// Where to find the template for the web.config that's copied to the webroot
+ /// </summary>
+ protected readonly string _templateWebConfig = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "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) :
+ base(pars.LogService, options, pars.Identifier)
+ {
+ _input = pars.InputService;
+ _proxy = pars.ProxyService;
+ _settings = pars.Settings;
+ _renewal = pars.Renewal;
+ _runLevel = runLevel;
+ _targetPart = pars.TargetPart;
+ _path = options.Path;
+ }
+
+ /// <summary>
+ /// Handle http challenge
+ /// </summary>
+ 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");
+ }
+
+ }
+
+ /// <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 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();
+ }
+ }
+ }
+
+ /// <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()
+ {
+ WriteFile(CombinePath(_path, _challenge.HttpResourcePath), _challenge.HttpResourceValue);
+ _challengeWritten = true;
+ }
+
+ /// <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()
+ {
+ 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;
+ }
+ }
+
+ /// <summary>
+ /// Get the template for the web.config
+ /// </summary>
+ /// <returns></returns>
+ private string GetWebConfig()
+ {
+ return File.ReadAllText(_templateWebConfig);
+ }
+
+ /// <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 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);
+ }
+ }
+
+ /// <summary>
+ /// Should delete any authorizations
+ /// </summary>
+ /// <param name="answerPath">where the answerFile should be located</param>
+ /// <param name="token">the token</param>
+ /// <param name="webRootPath">the website root path</param>
+ /// <param name="filePath">the file path for the authorization file</param>
+ 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);
+ }
+ }
+
+ /// <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 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;
+ }
+ }
+
+ /// <summary>
+ /// Write file with content to a specific location
+ /// </summary>
+ /// <param name="root"></param>
+ /// <param name="path"></param>
+ /// <param name="content"></param>
+ protected abstract void WriteFile(string path, string content);
+
+ /// <summary>
+ /// Delete file from specific location
+ /// </summary>
+ /// <param name="root"></param>
+ /// <param name="path"></param>
+ protected abstract void DeleteFile(string path);
+
+ /// <summary>
+ /// Check if folder is empty
+ /// </summary>
+ /// <param name="root"></param>
+ /// <param name="path"></param>
+ protected abstract bool IsEmpty(string path);
+
+ /// <summary>
+ /// Delete folder if not empty
+ /// </summary>
+ /// <param name="root"></param>
+ /// <param name="path"></param>
+ protected abstract void DeleteFolder(string path);
+
+ /// <summary>
+ /// Refresh
+ /// </summary>
+ /// <param name="scheduled"></param>
+ /// <returns></returns>
+ protected virtual void Refresh() { }
+
+ /// <summary>
+ /// Dispose
+ /// </summary>
+ public override void CleanUp()
+ {
+ DeleteWebConfig();
+ DeleteAuthorization();
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationArguments.cs b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationArguments.cs
new file mode 100644
index 0000000..4eb8390
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationArguments.cs
@@ -0,0 +1,9 @@
+namespace PKISharp.WACS.Plugins.ValidationPlugins
+{
+ class HttpValidationArguments
+ {
+ public string WebRoot { get; set; }
+ public bool Warmup { get; set; }
+ public bool ManualTargetIsIIS { get; set; }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationArgumentsProvider.cs b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationArgumentsProvider.cs
new file mode 100644
index 0000000..092f0f7
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationArgumentsProvider.cs
@@ -0,0 +1,33 @@
+using Fclp;
+using PKISharp.WACS.Configuration;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ class HttpValidationArgumentsProvider :
+ BaseArgumentsProvider<HttpValidationArguments>
+ {
+ public override string Group => "Validation";
+ public override string Name => "Common HTTP validation options";
+ public override string Condition => "--validation filesystem|ftp|sftp|webdav";
+
+ public override void Configure(FluentCommandLineParser<HttpValidationArguments> parser)
+ {
+ parser.Setup(o => o.WebRoot)
+ .As("webroot")
+ .WithDescription("Root path of the site that will serve the HTTP validation requests.");
+ parser.Setup(o => o.Warmup)
+ .As("warmup")
+ .WithDescription("Not used (warmup is the new default).");
+ parser.Setup(o => o.ManualTargetIsIIS)
+ .As("manualtargetisiis")
+ .WithDescription("Copy default web.config to the .well-known directory.");
+ }
+
+ public override bool Active(HttpValidationArguments current)
+ {
+ return !string.IsNullOrEmpty(current.WebRoot) ||
+ current.ManualTargetIsIIS ||
+ current.Warmup;
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationOptions.cs b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationOptions.cs
new file mode 100644
index 0000000..6546a9a
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationOptions.cs
@@ -0,0 +1,32 @@
+using PKISharp.WACS.Plugins.Base.Options;
+using PKISharp.WACS.Plugins.Interfaces;
+using PKISharp.WACS.Services;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins
+{
+ internal abstract class HttpValidationOptions<T> : ValidationPluginOptions<T> where T : IValidationPlugin
+ {
+ public string Path { get; set; }
+ public bool? CopyWebConfig { get; set; }
+
+ public HttpValidationOptions() { }
+ public HttpValidationOptions(HttpValidationOptions<T> source)
+ {
+ Path = source.Path;
+ CopyWebConfig = source.CopyWebConfig;
+ }
+
+ public override void Show(IInputService input)
+ {
+ base.Show(input);
+ if (!string.IsNullOrEmpty(Path))
+ {
+ input.Show("Path", Path, level: 1);
+ }
+ if (CopyWebConfig == true)
+ {
+ input.Show("Web.config", "Yes", level: 1);
+ }
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationOptionsFactory.cs
new file mode 100644
index 0000000..4b8979d
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationOptionsFactory.cs
@@ -0,0 +1,93 @@
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Plugins.Base.Factories;
+using PKISharp.WACS.Plugins.Interfaces;
+using PKISharp.WACS.Services;
+using System;
+using System.Collections.Generic;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins
+{
+ internal abstract class HttpValidationOptionsFactory<TPlugin, TOptions> :
+ ValidationPluginOptionsFactory<TPlugin, TOptions>
+ where TPlugin : IValidationPlugin
+ where TOptions : HttpValidationOptions<TPlugin>, new()
+ {
+ protected readonly IArgumentsService _arguments;
+
+ public HttpValidationOptionsFactory(IArgumentsService arguments)
+ {
+ _arguments = arguments;
+ }
+
+ /// <summary>
+ /// Get webroot path manually
+ /// </summary>
+ public HttpValidationOptions<TPlugin> BaseAquire(Target target, IInputService input, RunLevel runLevel)
+ {
+ var allowEmtpy = AllowEmtpy(target);
+ string path = _arguments.TryGetArgument(null, input, WebrootHint(allowEmtpy));
+ while (
+ (!string.IsNullOrEmpty(path) && !PathIsValid(path)) ||
+ (string.IsNullOrEmpty(path) && !allowEmtpy))
+ {
+ path = _arguments.TryGetArgument(null, input, WebrootHint(allowEmtpy));
+ }
+ return new TOptions {
+ Path = path,
+ CopyWebConfig = target.IIS || input.PromptYesNo("Copy default web.config before validation?", false)
+ };
+ }
+
+ /// <summary>
+ /// By default we don't allow emtpy paths, but FileSystem
+ /// makes an exception because it can read from IIS
+ /// </summary>
+ /// <param name="target"></param>
+ /// <returns></returns>
+ public virtual bool AllowEmtpy(Target target) => false;
+
+ /// <summary>
+ /// Check if the webroot makes sense
+ /// </summary>
+ /// <returns></returns>
+ public virtual bool PathIsValid(string path) => false;
+
+ /// <summary>
+ /// Get webroot automatically
+ /// </summary>
+ public HttpValidationOptions<TPlugin> BaseDefault(Target target)
+ {
+ string path = null;
+ var allowEmpty = AllowEmtpy(target);
+ var args = _arguments.GetArguments<HttpValidationArguments>();
+ if (string.IsNullOrEmpty(path) && !allowEmpty)
+ {
+ path = _arguments.TryGetRequiredArgument(nameof(args.WebRoot), args.WebRoot);
+ }
+ if (!string.IsNullOrEmpty(path) && !PathIsValid(path))
+ {
+ throw new ArgumentException($"Invalid webroot {path}: {WebrootHint(false)[0]}");
+ }
+ return new TOptions
+ {
+ Path = path,
+ CopyWebConfig = target.IIS || args.ManualTargetIsIIS
+ };
+ }
+
+ /// <summary>
+ /// Hint to show to the user what the webroot should look like
+ /// </summary>
+ /// <returns></returns>
+ public virtual string[] WebrootHint(bool allowEmpty)
+ {
+ var ret = new List<string> { "Path to the root of the site that will handle authentication" };
+ if (allowEmpty)
+ {
+ ret.Add("Leave empty to automatically read the path from IIS");
+ }
+ return ret.ToArray();
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationParameters.cs b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationParameters.cs
new file mode 100644
index 0000000..06a0968
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/HttpValidationParameters.cs
@@ -0,0 +1,37 @@
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Services;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins
+{
+ class HttpValidationParameters
+ {
+ public ISettingsService Settings { get; private set; }
+ public Renewal Renewal { get; private set; }
+ public TargetPart TargetPart { get; private set; }
+ public RunLevel RunLevel { get; private set; }
+ public string Identifier { get; private set; }
+ public ILogService LogService { get; private set; }
+ public IInputService InputService { get; private set; }
+ public ProxyService ProxyService { get; private set; }
+
+ public HttpValidationParameters(
+ ILogService log,
+ IInputService input,
+ ISettingsService settings,
+ ProxyService proxy,
+ Renewal renewal,
+ TargetPart target,
+ RunLevel runLevel,
+ string identifier)
+ {
+ Renewal = renewal;
+ TargetPart = target;
+ RunLevel = runLevel;
+ Identifier = identifier;
+ Settings = settings;
+ ProxyService = proxy;
+ LogService = log;
+ InputService = input;
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHosting.cs b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHosting.cs
new file mode 100644
index 0000000..1f5361e
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHosting.cs
@@ -0,0 +1,69 @@
+using ACMESharp.Authorizations;
+using PKISharp.WACS.Services;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ internal class SelfHosting : Validation<SelfHostingOptions, Http01ChallengeValidationDetails>
+ {
+ internal const int DefaultValidationPort = 80;
+ private HttpListener _listener;
+ private readonly Dictionary<string, string> _files;
+
+ public SelfHosting(string identifier, ILogService log, SelfHostingOptions options) :
+ base(log, options, identifier)
+ {
+ _files = new Dictionary<string, string>();
+ }
+
+ public async Task RecieveRequests()
+ {
+ while (_listener.IsListening)
+ {
+ var ctx = await _listener.GetContextAsync();
+ var path = ctx.Request.Url.LocalPath;
+ if (_files.TryGetValue(path, out string response))
+ {
+ _log.Verbose("SelfHosting plugin serving file {name}", path);
+ using (var writer = new StreamWriter(ctx.Response.OutputStream))
+ {
+ writer.Write(response);
+ }
+ }
+ else
+ {
+ _log.Warning("SelfHosting plugin couldn't serve file {name}", path);
+ ctx.Response.StatusCode = 404;
+ }
+ }
+ }
+
+ public override void CleanUp()
+ {
+ _listener.Stop();
+ _listener.Close();
+ _listener = null;
+ }
+
+ public override void PrepareChallenge()
+ {
+ _files.Add("/" + _challenge.HttpResourcePath, _challenge.HttpResourceValue);
+ try
+ {
+ var prefix = $"http://+:{_options.Port ?? DefaultValidationPort}/.well-known/acme-challenge/";
+ _listener = new HttpListener();
+ _listener.Prefixes.Add(prefix);
+ _listener.Start();
+ Task.Run(RecieveRequests);
+ }
+ catch
+ {
+ _log.Error("Unable to activate HttpListener, this may be due to non-Microsoft webserver using port 80");
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingArguments.cs b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingArguments.cs
new file mode 100644
index 0000000..14bfe1b
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingArguments.cs
@@ -0,0 +1,7 @@
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ class SelfHostingArguments
+ {
+ public int? ValidationPort { get; set; }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingArgumentsProvider.cs b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingArgumentsProvider.cs
new file mode 100644
index 0000000..feac957
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingArgumentsProvider.cs
@@ -0,0 +1,25 @@
+using Fclp;
+using PKISharp.WACS.Configuration;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ class SelfHostingArgumentsProvider : BaseArgumentsProvider<SelfHostingArguments>
+ {
+ public override string Name => "SelfHosting plugin";
+ public override string Group => "Validation";
+ public override string Condition => "--validation selfhosting";
+ public override bool Default => true;
+
+ public override void Configure(FluentCommandLineParser<SelfHostingArguments> parser)
+ {
+ parser.Setup(o => o.ValidationPort)
+ .As("validationport")
+ .WithDescription("Port to use for listening to validation requests. Note that the ACME server will always send requests to port 80. This option is only useful in combination with a port forwarding.");
+ }
+
+ public override bool Active(SelfHostingArguments current)
+ {
+ return current.ValidationPort != null;
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptions.cs b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptions.cs
new file mode 100644
index 0000000..0d8e562
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptions.cs
@@ -0,0 +1,33 @@
+using PKISharp.WACS.Plugins.Base;
+using PKISharp.WACS.Plugins.Base.Options;
+using PKISharp.WACS.Services;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ [Plugin("c7d5e050-9363-4ba1-b3a8-931b31c618b7")]
+ internal class SelfHostingOptions : ValidationPluginOptions<SelfHosting>
+ {
+ public override string Name { get => "SelfHosting"; }
+ public override string Description { get => "Serve verification files from memory (recommended)"; }
+
+ /// <summary>
+ /// Alternative port for validation. Note that ACME always requires
+ /// port 80 to be open. This is only useful if the port is interally
+ /// mapped/forwarded to a different one.
+ /// </summary>
+ public int? Port { get; set; }
+
+ /// <summary>
+ /// Show to use what has been configured
+ /// </summary>
+ /// <param name="input"></param>
+ public override void Show(IInputService input)
+ {
+ base.Show(input);
+ if (Port != null)
+ {
+ input.Show("Port", Port.ToString());
+ }
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptionsFactory.cs
new file mode 100644
index 0000000..2a3c5ab
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/SelfHosting/SelfHostingOptionsFactory.cs
@@ -0,0 +1,30 @@
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Plugins.Base.Factories;
+using PKISharp.WACS.Services;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ internal class SelfHostingOptionsFactory : ValidationPluginOptionsFactory<SelfHosting, SelfHostingOptions>
+ {
+ private readonly IArgumentsService _arguments;
+
+ public SelfHostingOptionsFactory(IArgumentsService arguments)
+ {
+ _arguments = arguments;
+ }
+
+ public override SelfHostingOptions Aquire(Target target, IInputService inputService, RunLevel runLevel)
+ {
+ return Default(target);
+ }
+
+ public override SelfHostingOptions Default(Target target)
+ {
+ var args = _arguments.GetArguments<SelfHostingArguments>();
+ return new SelfHostingOptions()
+ {
+ Port = args.ValidationPort
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs b/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs
new file mode 100644
index 0000000..150a3c0
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/Sftp.cs
@@ -0,0 +1,44 @@
+using PKISharp.WACS.Clients;
+using System.Linq;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ internal class Sftp : HttpValidation<SftpOptions, Sftp>
+ {
+ private SshFtpClient _sshFtpClient;
+
+ public Sftp(SftpOptions options, HttpValidationParameters pars, RunLevel runLevel) : base(options, runLevel, pars)
+ {
+ _sshFtpClient = new SshFtpClient(_options.Credential.GetCredential(), pars.LogService);
+ }
+
+ protected override char PathSeparator => '/';
+
+ protected override void DeleteFile(string path)
+ {
+ _sshFtpClient.Delete(path, SshFtpClient.FileType.File);
+ }
+
+ protected override void DeleteFolder(string path)
+ {
+ _sshFtpClient.Delete(path, SshFtpClient.FileType.Directory);
+ }
+
+ protected override bool IsEmpty(string path)
+ {
+ return !_sshFtpClient.GetFiles(path).Any();
+ }
+
+ protected override void WriteFile(string path, string content)
+ {
+ _sshFtpClient.Upload(path, content);
+ }
+
+ public override void CleanUp()
+ {
+ base.CleanUp();
+ // Switched setting this to null, since this class will be needed for deleting files and folder structure
+ _sshFtpClient = null;
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/SftpOptions.cs b/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/SftpOptions.cs
new file mode 100644
index 0000000..094566c
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/SftpOptions.cs
@@ -0,0 +1,31 @@
+using PKISharp.WACS.Configuration;
+using PKISharp.WACS.Plugins.Base;
+using PKISharp.WACS.Services;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ [Plugin("048aa2e7-2bce-4d3e-b731-6e0ed8b8170d")]
+ internal class SftpOptions : HttpValidationOptions<Sftp>
+ {
+ public override string Name { get => "SFTP"; }
+ public override string Description { get => "Upload verification files via SSH-FTP"; }
+
+ public SftpOptions() : base() { }
+ public SftpOptions(HttpValidationOptions<Sftp> source) : base(source) { }
+
+ /// <summary>
+ /// Credentials to use for WebDav connection
+ /// </summary>
+ public NetworkCredentialOptions Credential { get; set; }
+
+ /// <summary>
+ /// Show settings to user
+ /// </summary>
+ /// <param name="input"></param>
+ public override void Show(IInputService input)
+ {
+ base.Show(input);
+ Credential.Show(input);
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/SftpOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/SftpOptionsFactory.cs
new file mode 100644
index 0000000..aea3981
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/Sftp/SftpOptionsFactory.cs
@@ -0,0 +1,41 @@
+using PKISharp.WACS.Clients.IIS;
+using PKISharp.WACS.Configuration;
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Services;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ /// <summary>
+ /// Sftp validation
+ /// </summary>
+ internal class SftpOptionsFactory : HttpValidationOptionsFactory<Sftp, SftpOptions>
+ {
+ public SftpOptionsFactory(IArgumentsService arguments) : base(arguments) { }
+
+ public override bool PathIsValid(string path) => path.StartsWith("sftp://");
+
+ public override string[] WebrootHint(bool allowEmpty)
+ {
+ return new[] {
+ "Enter an sftp path that leads to the web root of the host for sftp authentication",
+ " Example, sftp://domain.com:22/site/wwwroot/"
+ };
+ }
+
+ public override SftpOptions Default(Target target)
+ {
+ return new SftpOptions(BaseDefault(target))
+ {
+ Credential = new NetworkCredentialOptions(_arguments)
+ };
+ }
+
+ public override SftpOptions Aquire(Target target, IInputService inputService, RunLevel runLevel)
+ {
+ return new SftpOptions(BaseAquire(target, inputService, runLevel))
+ {
+ Credential = new NetworkCredentialOptions(_arguments, inputService)
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDav.cs b/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDav.cs
new file mode 100644
index 0000000..f44f861
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDav.cs
@@ -0,0 +1,46 @@
+using PKISharp.WACS.Client;
+using PKISharp.WACS.Services;
+using System.Linq;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ internal class WebDav : HttpValidation<WebDavOptions, WebDav>
+ {
+ private WebDavClientWrapper _webdavClient;
+
+ public WebDav(
+ WebDavOptions options, HttpValidationParameters pars,
+ RunLevel runLevel, ProxyService proxy) :
+ base(options, runLevel, pars)
+ {
+ _webdavClient = new WebDavClientWrapper(_options.Credential, pars.LogService, proxy);
+ }
+
+ protected override void DeleteFile(string path)
+ {
+ _webdavClient.Delete(path);
+ }
+
+ protected override void DeleteFolder(string path)
+ {
+ _webdavClient.Delete(path);
+ }
+
+ protected override bool IsEmpty(string path)
+ {
+ return !_webdavClient.IsEmpty(path);
+ }
+
+ protected override char PathSeparator => '/';
+
+ protected override void WriteFile(string path, string content)
+ {
+ _webdavClient.Upload(path, content);
+ }
+ public override void CleanUp()
+ {
+ base.CleanUp();
+ _webdavClient = null;
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDavOptions.cs b/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDavOptions.cs
new file mode 100644
index 0000000..2e83328
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDavOptions.cs
@@ -0,0 +1,31 @@
+using PKISharp.WACS.Configuration;
+using PKISharp.WACS.Plugins.Base;
+using PKISharp.WACS.Services;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ [Plugin("7e191d0e-30d1-47b3-ae2e-442499d33e16")]
+ internal class WebDavOptions : HttpValidationOptions<WebDav>
+ {
+ public override string Name { get => "WebDav"; }
+ public override string Description { get => "Upload verification files via WebDav"; }
+
+ public WebDavOptions() : base() { }
+ public WebDavOptions(HttpValidationOptions<WebDav> source) : base(source) { }
+
+ /// <summary>
+ /// Credentials to use for WebDav connection
+ /// </summary>
+ public NetworkCredentialOptions Credential { get; set; }
+
+ /// <summary>
+ /// Show settings to user
+ /// </summary>
+ /// <param name="input"></param>
+ public override void Show(IInputService input)
+ {
+ base.Show(input);
+ Credential.Show(input);
+ }
+ }
+}
diff --git a/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDavOptionsFactory.cs b/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDavOptionsFactory.cs
new file mode 100644
index 0000000..d77d5d7
--- /dev/null
+++ b/src/main.lib/Plugins/ValidationPlugins/Http/WebDav/WebDavOptionsFactory.cs
@@ -0,0 +1,47 @@
+using PKISharp.WACS.Clients.IIS;
+using PKISharp.WACS.Configuration;
+using PKISharp.WACS.DomainObjects;
+using PKISharp.WACS.Services;
+
+namespace PKISharp.WACS.Plugins.ValidationPlugins.Http
+{
+ internal class WebDavOptionsFactory : HttpValidationOptionsFactory<WebDav, WebDavOptions>
+ {
+ public WebDavOptionsFactory(IArgumentsService arguments) : base(arguments) { }
+
+ public override bool PathIsValid(string webRoot)
+ {
+ return
+ webRoot.StartsWith("\\\\") ||
+ webRoot.StartsWith("dav://") ||
+ webRoot.StartsWith("webdav://") ||
+ webRoot.StartsWith("https://") ||
+ webRoot.StartsWith("http://");
+ }
+
+ public override string[] WebrootHint(bool allowEmtpy)
+ {
+ return new[] {
+ "Enter a webdav path that leads to the web root of the host for http authentication",
+ " Example, \\\\domain.com:80\\",
+ " Example, \\\\domain.com:443\\"
+ };
+ }
+
+ public override WebDavOptions Default(Target target)
+ {
+ return new WebDavOptions(BaseDefault(target))
+ {
+ Credential = new NetworkCredentialOptions(_arguments)
+ };
+ }
+
+ public override WebDavOptions Aquire(Target target, IInputService inputService, RunLevel runLevel)
+ {
+ return new WebDavOptions(BaseAquire(target, inputService, runLevel))
+ {
+ Credential = new NetworkCredentialOptions(_arguments, inputService)
+ };
+ }
+ }
+}