diff options
Diffstat (limited to 'src/main.lib/Plugins/ValidationPlugins/Http')
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) + }; + } + } +} |