From 78c7372a0d64e15734b14b0ca9852ae7c0a47132 Mon Sep 17 00:00:00 2001 From: ta264 Date: Thu, 14 May 2020 19:24:41 +0100 Subject: [PATCH] Fixed: Ensure SSL cert exists before saving config Trap missing certificate exception to avoid bootloop Fixes #4403 --- .../Validation/Paths/FileExistsValidator.cs | 26 ++++++++++++++++ .../WebHost/WebHostController.cs | 18 ++++++++++- src/Radarr.Api.V3/Config/HostConfigModule.cs | 30 +++++++++++++++++-- 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 src/NzbDrone.Core/Validation/Paths/FileExistsValidator.cs diff --git a/src/NzbDrone.Core/Validation/Paths/FileExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/FileExistsValidator.cs new file mode 100644 index 000000000..9adb200aa --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/FileExistsValidator.cs @@ -0,0 +1,26 @@ +using FluentValidation.Validators; +using NzbDrone.Common.Disk; + +namespace NzbDrone.Core.Validation.Paths +{ + public class FileExistsValidator : PropertyValidator + { + private readonly IDiskProvider _diskProvider; + + public FileExistsValidator(IDiskProvider diskProvider) + : base("File does not exist") + { + _diskProvider = diskProvider; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) + { + return false; + } + + return _diskProvider.FileExists(context.PropertyValue.ToString()); + } + } +} diff --git a/src/NzbDrone.Host/WebHost/WebHostController.cs b/src/NzbDrone.Host/WebHost/WebHostController.cs index 69ffa4e78..8e234ba16 100644 --- a/src/NzbDrone.Host/WebHost/WebHostController.cs +++ b/src/NzbDrone.Host/WebHost/WebHostController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -10,6 +11,7 @@ using NLog; using NLog.Extensions.Logging; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Exceptions; using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.Configuration; @@ -72,7 +74,21 @@ public void StartServer() { options.ConfigureHttpsDefaults(configureOptions => { - var certificate = new X509Certificate2(_configFileProvider.SslCertPath, _configFileProvider.SslCertPassword, X509KeyStorageFlags.DefaultKeySet); + X509Certificate2 certificate; + + try + { + certificate = new X509Certificate2(sslCertPath, _configFileProvider.SslCertPassword, X509KeyStorageFlags.DefaultKeySet); + } + catch (CryptographicException ex) + { + if (ex.HResult == 0x2 || ex.HResult == 0x2006D080) + { + throw new RadarrStartupException(ex, $"The SSL certificate file {sslCertPath} does not exist"); + } + + throw new RadarrStartupException(ex); + } configureOptions.ServerCertificate = certificate; }); diff --git a/src/Radarr.Api.V3/Config/HostConfigModule.cs b/src/Radarr.Api.V3/Config/HostConfigModule.cs index 1bd43d31c..571098326 100644 --- a/src/Radarr.Api.V3/Config/HostConfigModule.cs +++ b/src/Radarr.Api.V3/Config/HostConfigModule.cs @@ -1,6 +1,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Security.Cryptography.X509Certificates; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; @@ -18,7 +19,10 @@ public class HostConfigModule : RadarrRestModule private readonly IConfigService _configService; private readonly IUserService _userService; - public HostConfigModule(IConfigFileProvider configFileProvider, IConfigService configService, IUserService userService) + public HostConfigModule(IConfigFileProvider configFileProvider, + IConfigService configService, + IUserService userService, + FileExistsValidator fileExistsValidator) : base("/config/host") { _configFileProvider = configFileProvider; @@ -43,7 +47,14 @@ public HostConfigModule(IConfigFileProvider configFileProvider, IConfigService c SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl); SharedValidator.RuleFor(c => c.SslPort).NotEqual(c => c.Port).When(c => c.EnableSsl); - SharedValidator.RuleFor(c => c.SslCertPath).NotEmpty().When(c => c.EnableSsl); + + SharedValidator.RuleFor(c => c.SslCertPath) + .Cascade(CascadeMode.StopOnFirstFailure) + .NotEmpty() + .IsValidPath() + .SetValidator(fileExistsValidator) + .Must((resource, path) => IsValidSslCertificate(resource)).WithMessage("Invalid SSL certificate file or password") + .When(c => c.EnableSsl); SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default"); SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script); @@ -53,6 +64,21 @@ public HostConfigModule(IConfigFileProvider configFileProvider, IConfigService c SharedValidator.RuleFor(c => c.BackupRetention).InclusiveBetween(1, 90); } + private bool IsValidSslCertificate(HostConfigResource resource) + { + X509Certificate2 cert; + try + { + cert = new X509Certificate2(resource.SslCertPath, resource.SslCertPassword, X509KeyStorageFlags.DefaultKeySet); + } + catch + { + return false; + } + + return cert != null; + } + private HostConfigResource GetHostConfig() { var resource = _configFileProvider.ToResource(_configService);