using Microsoft.Extensions.Configuration;
using PKISharp.WACS.Extensions;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
namespace PKISharp.WACS.Services
{
public class SettingsService : ISettingsService
{
private readonly ILogService _log;
private readonly IArgumentsService _arguments;
public bool Valid { get; private set; } = false;
public ClientSettings Client { get; private set; } = new ClientSettings();
public UiSettings UI { get; private set; } = new UiSettings();
public AcmeSettings Acme { get; private set; } = new AcmeSettings();
public ProxySettings Proxy { get; private set; } = new ProxySettings();
public CacheSettings Cache { get; private set; } = new CacheSettings();
public ScheduledTaskSettings ScheduledTask { get; private set; } = new ScheduledTaskSettings();
public NotificationSettings Notification { get; private set; } = new NotificationSettings();
public SecuritySettings Security { get; private set; } = new SecuritySettings();
public ScriptSettings Script { get; private set; } = new ScriptSettings();
public TargetSettings Target { get; private set; } = new TargetSettings();
public ValidationSettings Validation { get; private set; } = new ValidationSettings();
public OrderSettings Order { get; private set; } = new OrderSettings();
public CsrSettings Csr { get; private set; } = new CsrSettings();
public StoreSettings Store { get; private set; } = new StoreSettings();
public InstallationSettings Installation { get; private set; } = new InstallationSettings();
public string ExePath { get; private set; } = Process.GetCurrentProcess().MainModule.FileName;
public SettingsService(ILogService log, IArgumentsService arguments)
{
_log = log;
_arguments = arguments;
var installDir = new FileInfo(ExePath).DirectoryName;
var settingsFileName = "settings.json";
var settingsFileTemplateName = "settings_default.json";
_log.Verbose($"Looking for {settingsFileName} in {installDir}");
var settings = new FileInfo(Path.Combine(installDir, settingsFileName));
var settingsTemplate = new FileInfo(Path.Combine(installDir, settingsFileTemplateName));
var useFile = settings;
if (!settings.Exists && settingsTemplate.Exists)
{
_log.Verbose($"Copying {settingsFileTemplateName} to {settingsFileName}");
try
{
settingsTemplate.CopyTo(settings.FullName);
}
catch (Exception)
{
_log.Error($"Unable to create {settingsFileName}, falling back to {settingsFileTemplateName}");
useFile = settingsTemplate;
}
}
try
{
new ConfigurationBuilder()
.AddJsonFile(useFile.FullName, true, true)
.Build()
.Bind(this);
}
catch (Exception ex)
{
_log.Error($"Unable to start program using {useFile.Name}");
while (ex.InnerException != null)
{
_log.Error(ex.InnerException.Message);
ex = ex.InnerException;
}
return;
}
CreateConfigPath();
CreateLogPath();
CreateCachePath();
Valid = true;
}
public Uri BaseUri
{
get
{
Uri? ret;
if (!string.IsNullOrEmpty(_arguments.MainArguments.BaseUri))
{
ret = new Uri(_arguments.MainArguments.BaseUri);
}
else if (_arguments.MainArguments.Test)
{
ret = Acme.DefaultBaseUriTest;
}
else
{
ret = Acme.DefaultBaseUri;
}
if (ret == null)
{
throw new Exception("Unable to determine BaseUri");
}
return ret;
}
}
///
/// Find and/or create path of the configuration files
///
///
private void CreateConfigPath()
{
var configRoot = "";
var userRoot = Client.ConfigurationPath;
if (!string.IsNullOrEmpty(userRoot))
{
configRoot = userRoot;
// Path configured in settings always wins, but
// check for possible sub directories with client name
// to keep bug-compatible with older releases that
// created a subfolder inside of the users chosen config path
var configRootWithClient = Path.Combine(userRoot, Client.ClientName);
if (Directory.Exists(configRootWithClient))
{
configRoot = configRootWithClient;
}
}
else
{
// When using a system folder, we have to create a sub folder
// with the most preferred client name, but we should check first
// if there is an older folder with an less preferred (older)
// client name.
// Stop looking if the directory has been found
if (!Directory.Exists(configRoot))
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
configRoot = Path.Combine(appData, Client.ClientName);
}
}
// This only happens when invalid options are provided
Client.ConfigurationPath = Path.Combine(configRoot, BaseUri.CleanUri());
// Create folder if it doesn't exist yet
var di = new DirectoryInfo(Client.ConfigurationPath);
if (!di.Exists)
{
try
{
Directory.CreateDirectory(Client.ConfigurationPath);
}
catch (Exception ex)
{
throw new Exception($"Unable to create configuration path {Client.ConfigurationPath}", ex);
}
}
_log.Debug("Config folder: {_configPath}", Client.ConfigurationPath);
}
///
/// Find and/or created path for logging
///
private void CreateLogPath()
{
if (string.IsNullOrWhiteSpace(Client.LogPath))
{
Client.LogPath = Path.Combine(Client.ConfigurationPath, "Log");
}
else
{
// Create seperate logs for each endpoint
Client.LogPath = Path.Combine(Client.LogPath, BaseUri.CleanUri());
}
if (!Directory.Exists(Client.LogPath))
{
try
{
Directory.CreateDirectory(Client.LogPath);
}
catch (Exception ex)
{
_log.Error(ex, "Unable to create log directory {_logPath}", Client.LogPath);
throw;
}
}
_log.Debug("Log path: {_logPath}", Client.LogPath);
}
///
/// Find and/or created path of the certificate cache
///
private void CreateCachePath()
{
if (string.IsNullOrWhiteSpace(Cache.Path))
{
Cache.Path = Path.Combine(Client.ConfigurationPath, "Certificates");
}
if (!Directory.Exists(Cache.Path))
{
try
{
Directory.CreateDirectory(Cache.Path);
}
catch (Exception ex)
{
_log.Error(ex, "Unable to create cache path {_certificatePath}", Cache.Path);
throw;
}
}
_log.Debug("Cache path: {_certificatePath}", Cache.Path);
}
public class ClientSettings
{
public string ClientName { get; set; } = "win-acme";
public string ConfigurationPath { get; set; } = "";
public string? LogPath { get; set; }
}
public class UiSettings
{
///
/// The number of hosts to display per page.
///
public int PageSize { get; set; } = 50;
///
/// A string that is used to format the date of the
/// pfx file friendly name. Documentation for
/// possibilities is available from Microsoft.
///
public string? DateFormat { get; set; }
///
/// How console tekst should be encoded
///
public string? TextEncoding { get; set; }
}
public class AcmeSettings
{
///
/// Default ACMEv2 endpoint to use when none
/// is specified with the command line.
///
public Uri? DefaultBaseUri { get; set; }
///
/// Default ACMEv2 endpoint to use when none is specified
/// with the command line and the --test switch is
/// activated.
///
public Uri? DefaultBaseUriTest { get; set; }
///
/// Default ACMEv1 endpoint to import renewal settings from.
///
public Uri? DefaultBaseUriImport { get; set; }
///
/// Use POST-as-GET request mode
///
public bool PostAsGet { get; set; }
///
/// Number of times wait for the ACME server to
/// handle validation and order processing
///
public int RetryCount { get; set; } = 4;
///
/// Amount of time (in seconds) to wait each
/// retry for the validation handling and order
/// processing
///
public int RetryInterval { get; set; } = 2;
}
public class ProxySettings
{
///
/// Configures a proxy server to use for
/// communication with the ACME server. The
/// default setting uses the system proxy.
/// Passing an empty string will bypass the
/// system proxy.
///
public string? Url { get; set; }
///
/// Username used to access the proxy server.
///
public string? Username { get; set; }
///
/// Password used to access the proxy server.
///
public string? Password { get; set; }
}
public class CacheSettings
{
///
/// The path where certificates and request files are
/// stored. If not specified or invalid, this defaults
/// to (ConfigurationPath)\Certificates. All directories
/// and subdirectories in the specified path are created
/// unless they already exist. If you are using a
/// [[Central SSL Store|Store-Plugins#centralssl]], this
/// can not be set to the same path.
///
public string? Path { get; set; }
///
/// When renewing or re-creating a previously
/// requested certificate that has the exact
/// same set of domain names, the program will
/// used a cached version for this many days,
/// to prevent users from running into rate
/// limits while experimenting. Set this to
/// a high value if you regularly re-request
/// the same certificates, e.g. for a Continuous
/// Deployment scenario.
///
public int ReuseDays { get; set; }
///
/// Automatically delete files older than 120 days
/// from the CertificatePath folder. Running with
/// default settings, these should only be long-
/// expired certificates, generated for abandoned
/// renewals. However we do advise caution.
///
public bool DeleteStaleFiles { get; set; }
}
public class ScheduledTaskSettings
{
///
/// The number of days to renew a certificate
/// after. Let’s Encrypt certificates are
/// currently for a max of 90 days so it is
/// advised to not increase the days much.
/// If you increase the days, please note
/// that you will have less time to fix any
/// issues if the certificate doesn’t renew
/// correctly.
///
public int RenewalDays { get; set; }
///
/// Configures random time to wait for starting
/// the scheduled task.
///
public TimeSpan RandomDelay { get; set; }
///
/// Configures start time for the scheduled task.
///
public TimeSpan StartBoundary { get; set; }
///
/// Configures time after which the scheduled
/// task will be terminated if it hangs for
/// whatever reason.
///
public TimeSpan ExecutionTimeLimit { get; set; }
}
public class NotificationSettings
{
///
/// SMTP server to use for sending email notifications.
/// Required to receive renewal failure notifications.
///
public string? SmtpServer { get; set; }
///
/// SMTP server port number.
///
public int SmtpPort { get; set; }
///
/// User name for the SMTP server, in case
/// of authenticated SMTP.
///
public string? SmtpUser { get; set; }
///
/// Password for the SMTP server, in case
/// of authenticated SMTP.
///
public string? SmtpPassword { get; set; }
///
/// Change to True to enable SMTPS.
///
public bool SmtpSecure { get; set; }
///
/// 1: Auto (default)
/// 2: SslOnConnect
/// 3: StartTls
/// 4: StartTlsWhenAvailable
///
public int? SmtpSecureMode { get; set; }
///
/// Display name to use as the sender of
/// notification emails. Defaults to the
/// ClientName setting when empty.
///
public string? SenderName { get; set; }
///
/// Email address to use as the sender
/// of notification emails. Required to
/// receive renewal failure notifications.
///
public string? SenderAddress { get; set; }
///
/// Email addresses to receive notification emails.
/// Required to receive renewal failure
/// notifications.
///
public List? ReceiverAddresses { get; set; }
///
/// Send an email notification when a certificate
/// has been successfully renewed, as opposed to
/// the default behavior that only send failure
/// notifications. Only works if at least
/// SmtpServer, SmtpSenderAddressand
/// SmtpReceiverAddress have been configured.
///
public bool EmailOnSuccess { get; set; }
///
/// Override the computer name that
/// is included in the body of the email
///
public string? ComputerName { get; set; }
}
public class SecuritySettings
{
///
/// The key size to sign the certificate with.
/// Minimum is 2048.
///
public int RSAKeyBits { get; set; }
///
/// The curve to use for EC certificates.
///
public string? ECCurve { get; set; }
///
/// If set to True, it will be possible to export
/// the generated certificates from the certificate
/// store, for example to move them to another
/// server.
///
public bool PrivateKeyExportable { get; set; }
///
/// Uses Microsoft Data Protection API to encrypt
/// sensitive parts of the configuration, e.g.
/// passwords. This may be disabled to share
/// the configuration across a cluster of machines.
///
public bool EncryptConfig { get; set; }
}
///
/// Options for installation and DNS scripts
///
public class ScriptSettings
{
public int Timeout { get; set; } = 600;
}
public class TargetSettings
{
///
/// Default plugin to select in the Advanced menu
/// in the menu.
public string? DefaultTarget { get; set; }
}
public class ValidationSettings
{
///
/// Default plugin to select in the Advanced menu (if
/// supported for the target), or when nothing is
/// specified on the command line.
///
public string? DefaultValidation { get; set; }
///
/// Default plugin type, e.g. HTTP-01 (default), DNS-01, etc.
///
public string? DefaultValidationMode { get; set; }
///
/// Disable multithreading for validation
///
public bool? DisableMultiThreading { get; set; }
///
/// If set to True, it will cleanup the folder structure
/// and files it creates under the site for authorization.
///
public bool CleanupFolders { get; set; }
///
/// If set to `true`, it will wait until it can verify that the
/// validation record has been created and is available before
/// beginning DNS validation.
///
public bool PreValidateDns { get; set; } = true;
///
/// Maximum numbers of times to retry DNS pre-validation, while
/// waiting for the name servers to start providing the expected
/// answer.
///
public int PreValidateDnsRetryCount { get; set; } = 5;
///
/// Amount of time in seconds to wait between each retry.
///
public int PreValidateDnsRetryInterval { get; set; } = 30;
///
/// If set to `true`, the program will attempt to recurively
/// follow CNAME records present on _acme-challenge subdomains to
/// find the final domain the DNS-01 challenge should be handled by.
/// This allows you to delegate validation of your certificates
/// to another domain or provider, which can have benefits for
/// security or save you the effort of having to move everything
/// to a party that supports automation.
///
public bool AllowDnsSubstitution { get; set; } = true;
///
/// A comma seperated list of servers to query during DNS
/// prevalidation checks to verify whether or not the validation
/// record has been properly created and is visible for the world.
/// These servers will be used to located the actual authoritative
/// name servers for the domain. You can use the string [System] to
/// have the program query your servers default, but note that this
/// can lead to prevalidation failures when your Active Directory is
/// hosting a private version of the DNS zone for internal use.
///
public List? DnsServers { get; set; }
}
public class OrderSettings
{
///
/// Default plugin to select when none is provided through the
/// command line
///
public string? DefaultPlugin { get; set; }
}
public class CsrSettings
{
///
/// Default plugin to select
///
public string? DefaultCsr { get; set; }
}
public class StoreSettings
{
///
/// Default plugin(s) to select
///
public string? DefaultStore { get; set; }
[Obsolete]
public string? DefaultCertificateStore { get; set; }
[Obsolete]
public string? DefaultCentralSslStore { get; set; }
[Obsolete]
public string? DefaultCentralSslPfxPassword { get; set; }
[Obsolete]
public string? DefaultPemFilesPath { get; set; }
///
/// Settings for the CentralSsl plugin
///
public CertificateStoreSettings? CertificateStore { get; set; }
///
/// Settings for the CentralSsl plugin
///
public CentralSslSettings? CentralSsl { get; set; }
///
/// Settings for the PemFiles plugin
///
public PemFilesSettings? PemFiles { get; set; }
///
/// Settings for the PfxFile plugin
///
public PfxFileSettings? PfxFile { get; set; }
}
public class CertificateStoreSettings
{
///
/// The certificate store to save the certificates in. If left empty,
/// certificates will be installed either in the WebHosting store,
/// or if that is not available, the My store (better known as Personal).
///
public string? DefaultStore { get; set; }
}
public class PemFilesSettings
{
///
/// When using --store pemfiles this path is used by default, saving
/// you the effort from providing it manually. Filling this out makes
/// the --pemfilespath parameter unnecessary in most cases. Renewals
/// created with the default path will automatically change to any
/// future default value, meaning this is also a good practice for
/// maintainability.
///
public string? DefaultPath{ get; set; }
}
public class CentralSslSettings
{
///
/// When using --store centralssl this path is used by default, saving you
/// the effort from providing it manually. Filling this out makes the
/// --centralsslstore parameter unnecessary in most cases. Renewals
/// created with the default path will automatically change to any
/// future default value, meaning this is also a good practice for
/// maintainability.
///
public string? DefaultPath { get; set; }
///
/// When using --store centralssl this password is used by default for
/// the pfx files, saving you the effort from providing it manually.
/// Filling this out makes the --pfxpassword parameter unnecessary in
/// most cases. Renewals created with the default password will
/// automatically change to any future default value, meaning this
/// is also a good practice for maintainability.
///
public string? DefaultPassword { get; set; }
}
public class PfxFileSettings
{
///
/// When using --store pfxfile this path is used by default, saving
/// you the effort from providing it manually. Filling this out makes
/// the --pfxfilepath parameter unnecessary in most cases. Renewals
/// created with the default path will automatically change to any
/// future default value, meaning this is also a good practice for
/// maintainability.
///
public string? DefaultPath { get; set; }
///
/// When using --store pfxfile this password is used by default for
/// the pfx files, saving you the effort from providing it manually.
/// Filling this out makes the --pfxpassword parameter unnecessary in
/// most cases. Renewals created with the default password will
/// automatically change to any future default value, meaning this
/// is also a good practice for maintainability.
///
public string? DefaultPassword { get; set; }
}
public class InstallationSettings
{
///
/// Default plugin(s) to select
///
public string? DefaultInstallation { get; set; }
}
}
}