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