private static void GenerateX509Certificate(string password, string filename) { Log.Information("Generating X509 certificate..."); filename = Path.Combine(AppContext.BaseDirectory, filename); var cert = X509.Generate(subject: AppName, password, X509KeyStorageFlags.Exportable); IOFile.WriteAllBytes(filename, cert.Export(X509ContentType.Pkcs12, password)); Log.Information($"Password: {password}"); Log.Information($"Certificate exported to {filename}"); }
/// <summary> /// Entrypoint. /// </summary> /// <param name="args">Command line arguments.</param> public static void Main(string[] args) { // populate the properties above so that we can override the default config file if needed, and to // check if the application is being run in command mode (run task and quit). EnvironmentVariables.Populate(prefix: EnvironmentVariablePrefix); Arguments.Populate(clearExistingValues: false); // if a user has used one of the arguments above, perform the requested task, then quit if (ShowVersion) { Log.Information(FullVersion); return; } if (ShowHelp || ShowEnvironmentVariables) { if (!NoLogo) { PrintLogo(FullVersion); } if (ShowHelp) { PrintCommandLineArguments(typeof(Options)); } if (ShowEnvironmentVariables) { PrintEnvironmentVariables(typeof(Options), EnvironmentVariablePrefix); } return; } if (GenerateCertificate) { GenerateX509Certificate(password: Cryptography.Random.GetBytes(16).ToBase62String(), filename: $"{AppName}.pfx"); return; } // the application isn't being run in command mode. derive the application directory value // and defaults that are dependent upon it AppDirectory ??= DefaultAppDirectory; DefaultConfigurationFile = Path.Combine(AppDirectory, $"{AppName}.yml"); DefaultDownloadsDirectory = Path.Combine(AppDirectory, "downloads"); DefaultIncompleteDirectory = Path.Combine(AppDirectory, "incomplete"); // the location of the configuration file might have been overriden by command line or envar. // if not, set it to the default. ConfigurationFile ??= DefaultConfigurationFile; // verify(create if needed) default application directories. if the downloads or complete // directories are overridden in config, those will be validated after the config is loaded. try { VerifyDirectory(AppDirectory, createIfMissing: true, verifyWriteable: true); VerifyDirectory(Path.Combine(AppDirectory, "data"), createIfMissing: true, verifyWriteable: true); VerifyDirectory(DefaultDownloadsDirectory, createIfMissing: true, verifyWriteable: true); VerifyDirectory(DefaultIncompleteDirectory, createIfMissing: true, verifyWriteable: true); } catch (Exception ex) { Log.Information($"Filesystem exception: {ex.Message}"); return; } // load and validate the configuration try { Configuration = new ConfigurationBuilder() .AddConfigurationProviders(EnvironmentVariablePrefix, ConfigurationFile) .Build(); Configuration.GetSection(AppName) .Bind(OptionsAtStartup, (o) => { o.BindNonPublicProperties = true; }); if (OptionsAtStartup.Debug) { Log.Information($"Configuration:\n{Configuration.GetDebugView()}"); } if (!OptionsAtStartup.TryValidate(out var result)) { Log.Information(result.GetResultView()); return; } } catch (Exception ex) { Log.Information($"Invalid configuration: {(!OptionsAtStartup.Debug ? ex : ex.Message)}"); return; } ConfigureGlobalLogger(); Log = Serilog.Log.ForContext(typeof(Program)); if (!OptionsAtStartup.Flags.NoLogo) { PrintLogo(FullVersion); } Log.Information("Version: {Version}", FullVersion); if (IsDevelopment) { Log.Warning("This is a Development build; YMMV"); } if (IsCanary) { Log.Warning("This is a canary build"); Log.Warning("Canary builds are considered UNSTABLE and may be completely BROKEN"); Log.Warning($"Please report any issues here: {IssuesUrl}"); } Log.Information("Invocation ID: {InvocationId}", InvocationId); Log.Information("Process ID: {ProcessId}", ProcessId); Log.Information("Instance Name: {InstanceName}", OptionsAtStartup.InstanceName); Log.Information("Using application directory {AppDirectory}", AppDirectory); Log.Information("Using configuration file {ConfigurationFile}", ConfigurationFile); RecreateConfigurationFileIfMissing(ConfigurationFile); if (!string.IsNullOrEmpty(OptionsAtStartup.Logger.Loki)) { Log.Information("Forwarding logs to Grafana Loki instance at {LoggerLokiUrl}", OptionsAtStartup.Logger.Loki); } // bootstrap the ASP.NET application try { if (OptionsAtStartup.Feature.Prometheus) { using var runtimeMetrics = DotNetRuntimeStatsBuilder.Default().StartCollecting(); } var builder = WebApplication.CreateBuilder(args); builder.Configuration .AddConfigurationProviders(EnvironmentVariablePrefix, ConfigurationFile); builder.Host .UseSerilog(); builder.WebHost .UseUrls() .UseKestrel(options => { Log.Information($"Listening for HTTP requests at http://{IPAddress.Any}:{OptionsAtStartup.Web.Port}/"); options.Listen(IPAddress.Any, OptionsAtStartup.Web.Port); if (!OptionsAtStartup.Web.Https.Disabled) { Log.Information($"Listening for HTTPS requests at https://{IPAddress.Any}:{OptionsAtStartup.Web.Https.Port}/"); options.Listen(IPAddress.Any, OptionsAtStartup.Web.Https.Port, listenOptions => { var cert = OptionsAtStartup.Web.Https.Certificate; if (!string.IsNullOrEmpty(cert.Pfx)) { Log.Information($"Using certificate from {cert.Pfx}"); listenOptions.UseHttps(cert.Pfx, cert.Password); } else { Log.Information($"Using randomly generated self-signed certificate"); listenOptions.UseHttps(X509.Generate(subject: AppName)); } }); } }); builder.Services .ConfigureAspDotNetServices() .ConfigureDependencyInjectionContainer(); var app = builder.Build(); app.ConfigureAspDotNetPipeline(); if (OptionsAtStartup.Flags.NoStart) { Log.Information("Qutting because 'no-start' option is enabled"); return; } app.Run(); } catch (Exception ex) { Log.Fatal(ex, "Application terminated unexpectedly"); } finally { Serilog.Log.CloseAndFlush(); } }
/// <summary> /// Entrypoint. /// </summary> /// <param name="args">Command line arguments.</param> public static void Main(string[] args) { // populate the properties above so that we can override the default config file if needed, and to // check if the application is being run in command mode (run task and quit). EnvironmentVariables.Populate(prefix: EnvironmentVariablePrefix); Arguments.Populate(clearExistingValues: false); if (ShowVersion) { Console.WriteLine(Version); return; } if (ShowHelp || ShowEnvironmentVariables) { if (!NoLogo) { PrintLogo(Version); } if (ShowHelp) { PrintCommandLineArguments(typeof(Options)); } if (ShowEnvironmentVariables) { PrintEnvironmentVariables(typeof(Options), EnvironmentVariablePrefix); } return; } if (GenerateCertificate) { GenerateX509Certificate(password: Guid.NewGuid().ToString(), filename: $"{AppName}.pfx"); return; } // the application isn't bein run in command mode. load all configuration values and proceed // with bootstrapping. try { Configuration = new ConfigurationBuilder() .AddConfigurationProviders(EnvironmentVariablePrefix, ConfigurationFile) .Build(); Configuration.GetSection(AppName) .Bind(Options, (o) => { o.BindNonPublicProperties = true; }); if (!Options.TryValidate(out var result)) { Console.WriteLine(result.GetResultView("Invalid configuration:")); return; } var cert = Options.Web.Https.Certificate; if (!string.IsNullOrEmpty(cert.Pfx) && !X509.TryValidate(cert.Pfx, cert.Password, out var certResult)) { Console.WriteLine($"Invalid HTTPs certificate: {certResult}"); return; } } catch (Exception ex) { Console.WriteLine($"Invalid configuration: {(!Options.Debug ? ex : ex.Message)}"); return; } if (!Options.NoLogo) { PrintLogo(Version); } if (Options.Debug) { Console.WriteLine("Configuration:"); Console.WriteLine(Configuration.GetDebugView()); Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("slskd.API.Authentication.PassthroughAuthenticationHandler", LogEventLevel.Information) .Enrich.WithProperty("Version", Version) .Enrich.WithProperty("InstanceName", Options.InstanceName) .Enrich.WithProperty("InvocationId", InvocationId) .Enrich.WithProperty("ProcessId", ProcessId) .Enrich.FromLogContext() .WriteTo.Console( outputTemplate: "[{SourceContext}] [{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") .WriteTo.Async(config => config.File( Path.Combine(AppContext.BaseDirectory, "logs", $"{AppName}-.log"), outputTemplate: "[{SourceContext}] [{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}", rollingInterval: RollingInterval.Day)) .WriteTo.Conditional( e => !string.IsNullOrEmpty(Options.Logger.Loki), config => config.GrafanaLoki( Options.Logger.Loki ?? string.Empty, outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")) .CreateLogger(); } else { Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("slskd.API.Authentication.PassthroughAuthenticationHandler", LogEventLevel.Information) .Enrich.WithProperty("Version", Version) .Enrich.WithProperty("InstanceName", Options.InstanceName) .Enrich.WithProperty("InvocationId", InvocationId) .Enrich.WithProperty("ProcessId", ProcessId) .Enrich.FromLogContext() .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") .WriteTo.Async(config => config.File( Path.Combine(AppContext.BaseDirectory, "logs", $"{AppName}-.log"), outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}", rollingInterval: RollingInterval.Day)) .WriteTo.Conditional( e => !string.IsNullOrEmpty(Options.Logger.Loki), config => config.GrafanaLoki( Options.Logger.Loki ?? string.Empty, outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")) .CreateLogger(); } var logger = Log.ForContext(typeof(Program)); if (ConfigurationFile != DefaultConfigurationFile && !File.Exists(ConfigurationFile)) { logger.Warning($"Specified configuration file '{ConfigurationFile}' could not be found and was not loaded."); } logger.Information("Version: {Version}", Version); logger.Information("Instance Name: {InstanceName}", Options.InstanceName); logger.Information("Invocation ID: {InvocationId}", InvocationId); logger.Information("Process ID: {ProcessId}", ProcessId); if (!string.IsNullOrEmpty(Options.Logger.Loki)) { logger.Information("Forwarding logs to Grafana Loki instance at {LoggerLokiUrl}", Options.Logger.Loki); } try { if (Options.Feature.Prometheus) { using var runtimeMetrics = DotNetRuntimeStatsBuilder.Default().StartCollecting(); } WebHost.CreateDefaultBuilder(args) .SuppressStatusMessages(true) .ConfigureAppConfiguration((hostingContext, builder) => { builder.Sources.Clear(); builder.AddConfigurationProviders(EnvironmentVariablePrefix, ConfigurationFile); }) .UseSerilog() .UseUrls() .UseKestrel(options => { logger.Information($"Listening for HTTP requests at http://{IPAddress.Any}:{Options.Web.Port}/"); options.Listen(IPAddress.Any, Options.Web.Port); logger.Information($"Listening for HTTPS requests at https://{IPAddress.Any}:{Options.Web.Https.Port}/"); options.Listen(IPAddress.Any, Options.Web.Https.Port, listenOptions => { var cert = Options.Web.Https.Certificate; if (!string.IsNullOrEmpty(cert.Pfx)) { logger.Information($"Using certificate from {cert.Pfx}"); listenOptions.UseHttps(cert.Pfx, cert.Password); } else { logger.Information($"Using randomly generated self-signed certificate"); listenOptions.UseHttps(X509.Generate(subject: AppName)); } }); }) .UseStartup <Startup>() .Build() .Run(); } catch (Exception ex) { logger.Fatal(ex, "Application terminated unexpectedly"); } finally { Log.CloseAndFlush(); } }