mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Add v5 General settings endpoints
This commit is contained in:
75
src/Sonarr.Api.V5/Settings/CertificateValidator.cs
Normal file
75
src/Sonarr.Api.V5/Settings/CertificateValidator.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Validators;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
||||
namespace Sonarr.Api.V5.Settings
|
||||
{
|
||||
public static class CertificateValidation
|
||||
{
|
||||
public static IRuleBuilderOptions<T, string> IsValidCertificate<T>(this IRuleBuilder<T, string> ruleBuilder)
|
||||
{
|
||||
return ruleBuilder.SetValidator(new CertificateValidator());
|
||||
}
|
||||
}
|
||||
|
||||
public class CertificateValidator : PropertyValidator
|
||||
{
|
||||
protected override string GetDefaultMessageTemplate() => "Invalid SSL certificate file or {passwordOrKey}. {message}";
|
||||
|
||||
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(CertificateValidator));
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
if (context.PropertyValue == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.InstanceToValidate is not GeneralSettingsResource resource)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var certPath = resource.SslCertPath!;
|
||||
var keyPath = resource.SslKeyPath;
|
||||
var certPassword = resource.SslCertPassword;
|
||||
var type = X509Certificate2.GetCertContentType(certPath);
|
||||
|
||||
try
|
||||
{
|
||||
if (type == X509ContentType.Cert)
|
||||
{
|
||||
X509Certificate2.CreateFromPemFile(certPath, keyPath.IsNullOrWhiteSpace() ? null : keyPath);
|
||||
}
|
||||
else if (type == X509ContentType.Pkcs12)
|
||||
{
|
||||
X509CertificateLoader.LoadPkcs12FromFile(certPath, certPassword, X509KeyStorageFlags.DefaultKeySet);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Debug("Invalid SSL certificate file. Unexpected certificate type: {0}", type);
|
||||
context.MessageFormatter.AppendArgument("passwordOrKey", "password");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
var passwordOrKey = type == X509ContentType.Cert ? "key" : "password";
|
||||
|
||||
Logger.Debug(ex, "Invalid SSL certificate file or {0}. {1}", passwordOrKey, ex.Message);
|
||||
|
||||
context.MessageFormatter.AppendArgument("passwordOrKey", passwordOrKey);
|
||||
context.MessageFormatter.AppendArgument("message", ex.Message);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
114
src/Sonarr.Api.V5/Settings/GeneralSettingsController.cs
Normal file
114
src/Sonarr.Api.V5/Settings/GeneralSettingsController.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Update;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using Sonarr.Http;
|
||||
|
||||
namespace Sonarr.Api.V5.Settings;
|
||||
|
||||
[V5ApiController("settings/general")]
|
||||
public class GeneralSettingsController : SettingsController<GeneralSettingsResource>
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public GeneralSettingsController(IConfigFileProvider configFileProvider,
|
||||
IConfigService configService,
|
||||
IUserService userService,
|
||||
IDiskProvider diskProvider)
|
||||
: base(configFileProvider, configService)
|
||||
{
|
||||
_userService = userService;
|
||||
|
||||
SharedValidator.RuleFor(c => c.BindAddress)
|
||||
.ValidIpAddress()
|
||||
.When(c => c.BindAddress != "*" && c.BindAddress != "localhost");
|
||||
|
||||
SharedValidator.RuleFor(c => c.Port).ValidPort();
|
||||
|
||||
SharedValidator.RuleFor(c => c.UrlBase).ValidUrlBase();
|
||||
SharedValidator.RuleFor(c => c.InstanceName).StartsOrEndsWithSonarr();
|
||||
|
||||
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod == AuthenticationType.Forms);
|
||||
SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod == AuthenticationType.Forms);
|
||||
|
||||
SharedValidator.RuleFor(c => c.AuthenticationMethod)
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
.NotEqual(AuthenticationType.Basic)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
.WithMessage("'Basic' is no longer supported, switch to 'Forms' instead.");
|
||||
|
||||
SharedValidator.RuleFor(c => c.PasswordConfirmation)
|
||||
.Must((resource, p) => IsMatchingPassword(resource)).WithMessage("Must match Password");
|
||||
|
||||
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)
|
||||
.Cascade(CascadeMode.Stop)
|
||||
.NotEmpty()
|
||||
.IsValidPath()
|
||||
.SetValidator(new FileExistsValidator(diskProvider))
|
||||
.IsValidCertificate()
|
||||
.When(c => c.EnableSsl);
|
||||
|
||||
SharedValidator.RuleFor(c => c.SslKeyPath)
|
||||
.NotEmpty()
|
||||
.IsValidPath()
|
||||
.SetValidator(new FileExistsValidator(diskProvider))
|
||||
.When(c => c.SslKeyPath.IsNotNullOrWhiteSpace());
|
||||
|
||||
SharedValidator.RuleFor(c => c.LogSizeLimit).InclusiveBetween(1, 10);
|
||||
|
||||
SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'main' is the default");
|
||||
SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script);
|
||||
|
||||
SharedValidator.RuleFor(c => c.BackupFolder).IsValidPath().When(c => Path.IsPathRooted(c.BackupFolder));
|
||||
SharedValidator.RuleFor(c => c.BackupInterval).InclusiveBetween(1, 7);
|
||||
SharedValidator.RuleFor(c => c.BackupRetention).InclusiveBetween(1, 90);
|
||||
}
|
||||
|
||||
private bool IsMatchingPassword(GeneralSettingsResource resource)
|
||||
{
|
||||
var user = _userService.FindUser();
|
||||
|
||||
if (user != null && user.Password == resource.Password)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (resource.Password == resource.PasswordConfirmation)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override GeneralSettingsResource ToResource(IConfigFileProvider configFile, IConfigService model)
|
||||
{
|
||||
var resource = GeneralSettingsResourceMapper.ToResource(configFile, model);
|
||||
|
||||
var user = _userService.FindUser();
|
||||
|
||||
resource.Username = user?.Username ?? string.Empty;
|
||||
resource.Password = user?.Password ?? string.Empty;
|
||||
resource.PasswordConfirmation = string.Empty;
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
public override ActionResult<GeneralSettingsResource> SaveSettings(GeneralSettingsResource resource)
|
||||
{
|
||||
if (resource.Username.IsNotNullOrWhiteSpace() && resource.Password.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_userService.Upsert(resource.Username, resource.Password);
|
||||
}
|
||||
|
||||
return base.SaveSettings(resource);
|
||||
}
|
||||
}
|
||||
93
src/Sonarr.Api.V5/Settings/GeneralSettingsResource.cs
Normal file
93
src/Sonarr.Api.V5/Settings/GeneralSettingsResource.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Security;
|
||||
using NzbDrone.Core.Update;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V5.Settings;
|
||||
|
||||
public class GeneralSettingsResource : RestResource
|
||||
{
|
||||
public string? BindAddress { get; set; }
|
||||
public int Port { get; set; }
|
||||
public int SslPort { get; set; }
|
||||
public bool EnableSsl { get; set; }
|
||||
public bool LaunchBrowser { get; set; }
|
||||
public AuthenticationType AuthenticationMethod { get; set; }
|
||||
public AuthenticationRequiredType AuthenticationRequired { get; set; }
|
||||
public bool AnalyticsEnabled { get; set; }
|
||||
public string? Username { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public string? PasswordConfirmation { get; set; }
|
||||
public string? LogLevel { get; set; }
|
||||
public int LogSizeLimit { get; set; }
|
||||
public string? ConsoleLogLevel { get; set; }
|
||||
public string? Branch { get; set; }
|
||||
public string? ApiKey { get; set; }
|
||||
public string? SslCertPath { get; set; }
|
||||
public string? SslKeyPath { get; set; }
|
||||
public string? SslCertPassword { get; set; }
|
||||
public string? UrlBase { get; set; }
|
||||
public string? InstanceName { get; set; }
|
||||
public string? ApplicationUrl { get; set; }
|
||||
public bool UpdateAutomatically { get; set; }
|
||||
public UpdateMechanism UpdateMechanism { get; set; }
|
||||
public string? UpdateScriptPath { get; set; }
|
||||
public bool ProxyEnabled { get; set; }
|
||||
public ProxyType ProxyType { get; set; }
|
||||
public string? ProxyHostname { get; set; }
|
||||
public int ProxyPort { get; set; }
|
||||
public string? ProxyUsername { get; set; }
|
||||
public string? ProxyPassword { get; set; }
|
||||
public string? ProxyBypassFilter { get; set; }
|
||||
public bool ProxyBypassLocalAddresses { get; set; }
|
||||
public CertificateValidationType CertificateValidation { get; set; }
|
||||
public string? BackupFolder { get; set; }
|
||||
public int BackupInterval { get; set; }
|
||||
public int BackupRetention { get; set; }
|
||||
}
|
||||
|
||||
public static class GeneralSettingsResourceMapper
|
||||
{
|
||||
public static GeneralSettingsResource ToResource(IConfigFileProvider model, IConfigService configService)
|
||||
{
|
||||
return new GeneralSettingsResource
|
||||
{
|
||||
BindAddress = model.BindAddress,
|
||||
Port = model.Port,
|
||||
SslPort = model.SslPort,
|
||||
EnableSsl = model.EnableSsl,
|
||||
LaunchBrowser = model.LaunchBrowser,
|
||||
AuthenticationMethod = model.AuthenticationMethod,
|
||||
AuthenticationRequired = model.AuthenticationRequired,
|
||||
AnalyticsEnabled = model.AnalyticsEnabled,
|
||||
LogLevel = model.LogLevel,
|
||||
LogSizeLimit = model.LogSizeLimit,
|
||||
ConsoleLogLevel = model.ConsoleLogLevel,
|
||||
Branch = model.Branch,
|
||||
ApiKey = model.ApiKey,
|
||||
SslCertPath = model.SslCertPath,
|
||||
SslKeyPath = model.SslKeyPath,
|
||||
SslCertPassword = model.SslCertPassword,
|
||||
UrlBase = model.UrlBase,
|
||||
InstanceName = model.InstanceName,
|
||||
UpdateAutomatically = model.UpdateAutomatically,
|
||||
UpdateMechanism = model.UpdateMechanism,
|
||||
UpdateScriptPath = model.UpdateScriptPath,
|
||||
ProxyEnabled = configService.ProxyEnabled,
|
||||
ProxyType = configService.ProxyType,
|
||||
ProxyHostname = configService.ProxyHostname,
|
||||
ProxyPort = configService.ProxyPort,
|
||||
ProxyUsername = configService.ProxyUsername,
|
||||
ProxyPassword = configService.ProxyPassword,
|
||||
ProxyBypassFilter = configService.ProxyBypassFilter,
|
||||
ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses,
|
||||
CertificateValidation = configService.CertificateValidation,
|
||||
BackupFolder = configService.BackupFolder,
|
||||
BackupInterval = configService.BackupInterval,
|
||||
BackupRetention = configService.BackupRetention,
|
||||
ApplicationUrl = configService.ApplicationUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user