1
0
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:
Mark McDowall
2026-02-16 14:24:08 -08:00
parent ac1c74105f
commit dd6533c18a
3 changed files with 282 additions and 0 deletions

View 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;
}
}
}
}

View 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);
}
}

View 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
};
}
}