mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-18 21:35:27 -04:00
@@ -156,6 +156,7 @@ function GeneralSettings() {
|
||||
enableSsl={settings.enableSsl}
|
||||
sslPort={settings.sslPort}
|
||||
sslCertPath={settings.sslCertPath}
|
||||
sslKeyPath={settings.sslKeyPath}
|
||||
sslCertPassword={settings.sslCertPassword}
|
||||
launchBrowser={settings.launchBrowser}
|
||||
onInputChange={handleInputChange}
|
||||
|
||||
@@ -19,6 +19,7 @@ interface HostSettingsProps {
|
||||
applicationUrl: PendingSection<General>['applicationUrl'];
|
||||
enableSsl: PendingSection<General>['enableSsl'];
|
||||
sslPort: PendingSection<General>['sslPort'];
|
||||
sslKeyPath: PendingSection<General>['sslKeyPath'];
|
||||
sslCertPath: PendingSection<General>['sslCertPath'];
|
||||
sslCertPassword: PendingSection<General>['sslCertPassword'];
|
||||
launchBrowser: PendingSection<General>['launchBrowser'];
|
||||
@@ -34,6 +35,7 @@ function HostSettings({
|
||||
enableSsl,
|
||||
sslPort,
|
||||
sslCertPath,
|
||||
sslKeyPath,
|
||||
sslCertPassword,
|
||||
launchBrowser,
|
||||
onInputChange,
|
||||
@@ -142,33 +144,46 @@ function HostSettings({
|
||||
) : null}
|
||||
|
||||
{enableSsl.value ? (
|
||||
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('SslCertPath')}</FormLabel>
|
||||
<>
|
||||
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('SslCertPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="sslCertPath"
|
||||
helpText={translate('SslCertPathHelpText')}
|
||||
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...sslCertPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : null}
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="sslCertPath"
|
||||
helpText={translate('SslCertPathHelpText')}
|
||||
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...sslCertPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{enableSsl.value ? (
|
||||
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('SslCertPassword')}</FormLabel>
|
||||
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('SslKeyPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="sslCertPassword"
|
||||
helpText={translate('SslCertPasswordHelpText')}
|
||||
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...sslCertPassword}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="sslKeyPath"
|
||||
helpText={translate('SslKeyPathHelpText')}
|
||||
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...sslKeyPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('SslCertPassword')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="sslCertPassword"
|
||||
helpText={translate('SslCertPasswordHelpText')}
|
||||
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...sslCertPassword}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{isWindowsService ? null : (
|
||||
|
||||
@@ -23,6 +23,7 @@ export default interface General {
|
||||
branch: string;
|
||||
apiKey: string;
|
||||
sslCertPath: string;
|
||||
sslKeyPath: string;
|
||||
sslCertPassword: string;
|
||||
urlBase: string;
|
||||
instanceName: string;
|
||||
|
||||
@@ -8,5 +8,6 @@ public class ServerOptions
|
||||
public bool? EnableSsl { get; set; }
|
||||
public int? SslPort { get; set; }
|
||||
public string SslCertPath { get; set; }
|
||||
public string SslKeyPath { get; set; }
|
||||
public string SslCertPassword { get; set; }
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ namespace NzbDrone.Core.Configuration
|
||||
string Branch { get; }
|
||||
string ApiKey { get; }
|
||||
string SslCertPath { get; }
|
||||
string SslKeyPath { get; }
|
||||
string SslCertPassword { get; }
|
||||
string UrlBase { get; }
|
||||
string UiFolder { get; }
|
||||
@@ -257,6 +258,7 @@ namespace NzbDrone.Core.Configuration
|
||||
public int LogSizeLimit => Math.Min(Math.Max(_logOptions.SizeLimit ?? GetValueInt("LogSizeLimit", 1, persist: false), 0), 10);
|
||||
public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||
public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", "");
|
||||
public string SslKeyPath => _serverOptions.SslKeyPath ?? GetValue("SslKeyPath", "");
|
||||
public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", "");
|
||||
|
||||
public string UrlBase
|
||||
|
||||
@@ -2002,7 +2002,9 @@
|
||||
"SslCertPassword": "SSL Cert Password",
|
||||
"SslCertPasswordHelpText": "Password for pfx file",
|
||||
"SslCertPath": "SSL Cert Path",
|
||||
"SslCertPathHelpText": "Path to pfx file",
|
||||
"SslCertPathHelpText": "Path to pfx or pem file",
|
||||
"SslKeyPath": "SSL Key Path",
|
||||
"SslKeyPathHelpText": "Path to key file used with pem file",
|
||||
"SslPort": "SSL Port",
|
||||
"Standard": "Standard",
|
||||
"StandardEpisodeFormat": "Standard Episode Format",
|
||||
|
||||
@@ -139,6 +139,7 @@ namespace NzbDrone.Host
|
||||
var sslPort = config.GetValue<int?>($"Sonarr:Server:{nameof(ServerOptions.SslPort)}") ?? config.GetValue(nameof(ConfigFileProvider.SslPort), 9898);
|
||||
var enableSsl = config.GetValue<bool?>($"Sonarr:Server:{nameof(ServerOptions.EnableSsl)}") ?? config.GetValue(nameof(ConfigFileProvider.EnableSsl), false);
|
||||
var sslCertPath = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.SslCertPath)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath));
|
||||
var sslKeyPath = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.SslKeyPath)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslKeyPath));
|
||||
var sslCertPassword = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.SslCertPassword)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword));
|
||||
var logDbEnabled = config.GetValue<bool?>($"Sonarr:Log:{nameof(LogOptions.DbEnabled)}") ?? config.GetValue(nameof(ConfigFileProvider.LogDbEnabled), true);
|
||||
|
||||
@@ -191,7 +192,7 @@ namespace NzbDrone.Host
|
||||
{
|
||||
options.ConfigureHttpsDefaults(configureOptions =>
|
||||
{
|
||||
configureOptions.ServerCertificate = ValidateSslCertificate(sslCertPath, sslCertPassword);
|
||||
configureOptions.ServerCertificate = ValidateSslCertificate(sslCertPath, sslKeyPath, sslCertPassword);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -271,13 +272,26 @@ namespace NzbDrone.Host
|
||||
return $"{scheme}://{bindAddress}:{port}";
|
||||
}
|
||||
|
||||
private static X509Certificate2 ValidateSslCertificate(string cert, string password)
|
||||
private static X509Certificate2 ValidateSslCertificate(string cert, string key, string password)
|
||||
{
|
||||
X509Certificate2 certificate;
|
||||
|
||||
try
|
||||
{
|
||||
certificate = new X509Certificate2(cert, password, X509KeyStorageFlags.DefaultKeySet);
|
||||
var type = X509Certificate2.GetCertContentType(cert);
|
||||
|
||||
if (type == X509ContentType.Cert)
|
||||
{
|
||||
certificate = X509Certificate2.CreateFromPemFile(cert, key.IsNullOrWhiteSpace() ? null : key);
|
||||
}
|
||||
else if (type == X509ContentType.Pkcs12)
|
||||
{
|
||||
certificate = new X509Certificate2(cert, password, X509KeyStorageFlags.DefaultKeySet);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new SonarrStartupException($"Invalid certificate type: {type}");
|
||||
}
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
@@ -289,6 +303,10 @@ namespace NzbDrone.Host
|
||||
|
||||
throw new SonarrStartupException(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new SonarrStartupException(ex);
|
||||
}
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Security.Cryptography.X509Certificates;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Validators;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
||||
namespace Sonarr.Api.V3.Config
|
||||
@@ -17,7 +18,7 @@ namespace Sonarr.Api.V3.Config
|
||||
|
||||
public class CertificateValidator : PropertyValidator
|
||||
{
|
||||
protected override string GetDefaultMessageTemplate() => "Invalid SSL certificate file or password. {message}";
|
||||
protected override string GetDefaultMessageTemplate() => "Invalid SSL certificate file or {passwordOrKey}. {message}";
|
||||
|
||||
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(CertificateValidator));
|
||||
|
||||
@@ -33,16 +34,38 @@ namespace Sonarr.Api.V3.Config
|
||||
return true;
|
||||
}
|
||||
|
||||
var certPath = resource.SslCertPath;
|
||||
var keyPath = resource.SslKeyPath;
|
||||
var certPassword = resource.SslCertPassword;
|
||||
var type = X509Certificate2.GetCertContentType(certPath);
|
||||
|
||||
try
|
||||
{
|
||||
new X509Certificate2(resource.SslCertPath, resource.SslCertPassword, X509KeyStorageFlags.DefaultKeySet);
|
||||
if (type == X509ContentType.Cert)
|
||||
{
|
||||
X509Certificate2.CreateFromPemFile(certPath, keyPath.IsNullOrWhiteSpace() ? null : keyPath);
|
||||
}
|
||||
else if (type == X509ContentType.Pkcs12)
|
||||
{
|
||||
new X509Certificate2(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)
|
||||
{
|
||||
Logger.Debug(ex, "Invalid SSL certificate file or password. {0}", ex.Message);
|
||||
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;
|
||||
|
||||
@@ -63,6 +63,12 @@ namespace Sonarr.Api.V3.Config
|
||||
.IsValidCertificate()
|
||||
.When(c => c.EnableSsl);
|
||||
|
||||
SharedValidator.RuleFor(c => c.SslKeyPath)
|
||||
.NotEmpty()
|
||||
.IsValidPath()
|
||||
.SetValidator(fileExistsValidator)
|
||||
.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");
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace Sonarr.Api.V3.Config
|
||||
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; }
|
||||
@@ -72,6 +73,7 @@ namespace Sonarr.Api.V3.Config
|
||||
Branch = model.Branch,
|
||||
ApiKey = model.ApiKey,
|
||||
SslCertPath = model.SslCertPath,
|
||||
SslKeyPath = model.SslKeyPath,
|
||||
SslCertPassword = model.SslCertPassword,
|
||||
UrlBase = model.UrlBase,
|
||||
InstanceName = model.InstanceName,
|
||||
|
||||
Reference in New Issue
Block a user