using System.Text; using System.Text.Json; using Hangfire; using Hangfire.EntityFrameworkCore; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using MoonCore.EnvConfiguration; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Extensions; using MoonCore.Extended.Helpers; using MoonCore.Extended.JwtInvalidation; using MoonCore.Extensions; using MoonCore.Helpers; using MoonCore.Logging; using MoonCore.Permissions; using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database; using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Implementations; using Moonlight.ApiServer.Implementations.Startup; using Moonlight.ApiServer.Interfaces; using Moonlight.ApiServer.Plugins; namespace Moonlight.ApiServer; // Cry about it #pragma warning disable ASP0000 public class Startup { private string[] Args; // Logging private ILoggerProvider[] LoggerProviders; private ILoggerFactory LoggerFactory; private ILogger Logger; // Configuration private AppConfiguration Configuration; private IConfigurationRoot ConfigurationRoot; // WebApplication Stuff private WebApplication WebApplication; private WebApplicationBuilder WebApplicationBuilder; // Plugin Loading private IPluginStartup[] PluginStartups; private IPluginStartup[] AdditionalPlugins; private IServiceProvider PluginLoadServiceProvider; public async Task Run(string[] args, IPluginStartup[]? additionalPlugins = null) { Args = args; AdditionalPlugins = additionalPlugins ?? []; await PrintVersion(); await CreateStorage(); await SetupAppConfiguration(); await SetupLogging(); await InitializePlugins(); await CreateWebApplicationBuilder(); await ConfigureKestrel(); await RegisterAppConfiguration(); await RegisterLogging(); await RegisterBase(); await RegisterDatabase(); await RegisterAuth(); await RegisterCors(); await RegisterHangfire(); await HookPluginBuild(); await RegisterPluginAssets(); await BuildWebApplication(); await PrepareDatabase(); await UseCors(); await UseBase(); await UseAuth(); await UseHangfire(); await HookPluginConfigure(); await MapBase(); await HookPluginEndpoints(); await WebApplication.RunAsync(); } private Task PrintVersion() { // Fancy start console output... yes very fancy :> var rainbow = new Crayon.Rainbow(0.5); foreach (var c in "Moonlight") { Console.Write( rainbow .Next() .Bold() .Text(c.ToString()) ); } Console.WriteLine(); return Task.CompletedTask; } private Task CreateStorage() { Directory.CreateDirectory("storage"); Directory.CreateDirectory(Path.Combine("storage", "logs")); return Task.CompletedTask; } #region Base private Task RegisterBase() { WebApplicationBuilder.Services.AutoAddServices(); WebApplicationBuilder.Services.AddHttpClient(); WebApplicationBuilder.Services.AddApiExceptionHandler(); // Add pre-existing services WebApplicationBuilder.Services.AddSingleton(Configuration); // Configure controllers var mvcBuilder = WebApplicationBuilder.Services.AddControllers(); // Add plugin assemblies as application parts foreach (var pluginStartup in PluginStartups.Select(x => x.GetType().Assembly).Distinct()) mvcBuilder.AddApplicationPart(pluginStartup.GetType().Assembly); return Task.CompletedTask; } private Task UseBase() { WebApplication.UseRouting(); WebApplication.UseExceptionHandler(); if (Configuration.Client.Enable) { if (WebApplication.Environment.IsDevelopment()) WebApplication.UseWebAssemblyDebugging(); WebApplication.UseBlazorFrameworkFiles(); WebApplication.UseStaticFiles(); } return Task.CompletedTask; } private Task MapBase() { WebApplication.MapControllers(); if (Configuration.Client.Enable) WebApplication.MapFallbackToController("Index", "Frontend"); return Task.CompletedTask; } private Task ConfigureKestrel() { WebApplicationBuilder.WebHost.ConfigureKestrel(kestrelOptions => { var maxUploadInBytes = ByteConverter .FromMegaBytes(Configuration.Kestrel.UploadLimit) .Bytes; kestrelOptions.Limits.MaxRequestBodySize = maxUploadInBytes; }); return Task.CompletedTask; } #endregion #region Plugin Loading private Task InitializePlugins() { // Create service provider for starting up var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(Configuration); serviceCollection.AddLogging(builder => { builder.ClearProviders(); builder.AddAnsiConsole(); }); PluginLoadServiceProvider = serviceCollection.BuildServiceProvider(); // Collect startups var pluginStartups = new List(); pluginStartups.AddRange(AdditionalPlugins); // Used by the development server // Do NOT remove the following comment, as its used to place the plugin startup register calls // MLBUILD_PLUGIN_STARTUP_HERE PluginStartups = pluginStartups.ToArray(); return Task.CompletedTask; } private Task RegisterPluginAssets() { return Task.CompletedTask; } #region Hooks private async Task HookPluginBuild() { foreach (var pluginAppStartup in PluginStartups) { try { await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebApplicationBuilder); } catch (Exception e) { Logger.LogError( "An error occured while processing 'BuildApp' for '{name}': {e}", pluginAppStartup.GetType().FullName, e ); } } } private async Task HookPluginConfigure() { foreach (var pluginAppStartup in PluginStartups) { try { await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebApplication); } catch (Exception e) { Logger.LogError( "An error occured while processing 'ConfigureApp' for '{name}': {e}", pluginAppStartup.GetType().FullName, e ); } } } private async Task HookPluginEndpoints() { foreach (var pluginEndpointStartup in PluginStartups) { try { await pluginEndpointStartup.ConfigureEndpoints(PluginLoadServiceProvider, WebApplication); } catch (Exception e) { Logger.LogError( "An error occured while processing 'ConfigureEndpoints' for '{name}': {e}", pluginEndpointStartup.GetType().FullName, e ); } } } #endregion #endregion #region Configurations private async Task SetupAppConfiguration() { // Configure configuration (wow) var configurationBuilder = new ConfigurationBuilder(); // Ensure configuration file exists var jsonFilePath = Path.Combine(Directory.GetCurrentDirectory(), "storage", "app.json"); if (!File.Exists(jsonFilePath)) await File.WriteAllTextAsync(jsonFilePath, JsonSerializer.Serialize(new AppConfiguration())); configurationBuilder.AddJsonFile( jsonFilePath ); configurationBuilder.AddEnvironmentVariables(prefix: "MOONLIGHT_", separator: "_"); ConfigurationRoot = configurationBuilder.Build(); // Retrieve configuration Configuration = ConfigurationRoot.Get()!; } private Task RegisterAppConfiguration() { WebApplicationBuilder.Services.AddSingleton(Configuration); return Task.CompletedTask; } #endregion #region Web Application private Task CreateWebApplicationBuilder() { WebApplicationBuilder = WebApplication.CreateBuilder(Args); return Task.CompletedTask; } private Task BuildWebApplication() { WebApplication = WebApplicationBuilder.Build(); return Task.CompletedTask; } #endregion #region Logging private Task SetupLogging() { LoggerFactory = new LoggerFactory(); LoggerFactory.AddAnsiConsole(); Logger = LoggerFactory.CreateLogger(); return Task.CompletedTask; } private async Task RegisterLogging() { // Configure application logging WebApplicationBuilder.Logging.ClearProviders(); WebApplicationBuilder.Logging.AddAnsiConsole(); WebApplicationBuilder.Logging.AddFile(Path.Combine("storage", "logs", "moonlight.log")); // Logging levels var logConfigPath = Path.Combine("storage", "logConfig.json"); // Ensure logging config, add a default one is missing if (!File.Exists(logConfigPath)) { var defaultLogLevels = new Dictionary { { "Default", "Information" }, { "Microsoft.AspNetCore", "Warning" }, { "System.Net.Http.HttpClient", "Warning" } }; var logLevelsJson = JsonSerializer.Serialize(defaultLogLevels); await File.WriteAllTextAsync(logConfigPath, logLevelsJson); } // Add logging configuration var logLevels = JsonSerializer.Deserialize>( await File.ReadAllTextAsync(logConfigPath) )!; foreach (var level in logLevels) WebApplicationBuilder.Logging.AddFilter(level.Key, Enum.Parse(level.Value)); // Mute exception handler middleware // https://github.com/dotnet/aspnetcore/issues/19740 WebApplicationBuilder.Logging.AddFilter( "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware", LogLevel.Critical ); WebApplicationBuilder.Logging.AddFilter( "Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware", LogLevel.Critical ); } #endregion #region Database private Task RegisterDatabase() { WebApplicationBuilder.Services.AddDatabaseMappings(); WebApplicationBuilder.Services.AddServiceCollectionAccessor(); WebApplicationBuilder.Services.AddScoped(typeof(DatabaseRepository<>)); return Task.CompletedTask; } private async Task PrepareDatabase() { await WebApplication.Services.EnsureDatabaseMigrated(); WebApplication.Services.GenerateDatabaseMappings(); } #endregion #region Authentication & Authorisation private Task RegisterAuth() { WebApplicationBuilder.Services .AddAuthentication("coreAuthentication") .AddJwtBearer("coreAuthentication", options => { options.TokenValidationParameters = new() { IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes( Configuration.Authentication.Secret )), ValidateIssuerSigningKey = true, ValidateLifetime = true, ClockSkew = TimeSpan.Zero, ValidateAudience = true, ValidAudience = Configuration.PublicUrl, ValidateIssuer = true, ValidIssuer = Configuration.PublicUrl }; }); WebApplicationBuilder.Services.AddJwtBearerInvalidation("coreAuthentication"); WebApplicationBuilder.Services.AddScoped(); WebApplicationBuilder.Services.AddAuthorization(); WebApplicationBuilder.Services.AddAuthorizationPermissions(options => { options.ClaimName = "permissions"; options.Prefix = "permissions:"; }); // Add local oauth2 provider if enabled if (Configuration.Authentication.EnableLocalOAuth2) WebApplicationBuilder.Services.AddScoped(); return Task.CompletedTask; } private Task UseAuth() { WebApplication.UseAuthentication(); WebApplication.UseAuthorization(); return Task.CompletedTask; } #endregion #region Cors private Task RegisterCors() { var allowedOrigins = Configuration.Kestrel.AllowedOrigins.Split(";", StringSplitOptions.RemoveEmptyEntries); WebApplicationBuilder.Services.AddCors(options => { var cors = new CorsPolicyBuilder(); if (allowedOrigins.Contains("*")) { cors.SetIsOriginAllowed(_ => true) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); } else { cors.WithOrigins(allowedOrigins) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); } options.AddDefaultPolicy( cors.Build() ); }); return Task.CompletedTask; } private Task UseCors() { WebApplication.UseCors(); return Task.CompletedTask; } #endregion #region Hangfire private Task RegisterHangfire() { WebApplicationBuilder.Services.AddHangfire((provider, configuration) => { configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_180); configuration.UseSimpleAssemblyNameTypeSerializer(); configuration.UseRecommendedSerializerSettings(); configuration.UseEFCoreStorage(() => { var scope = provider.CreateScope(); return scope.ServiceProvider.GetRequiredService(); }, new EFCoreStorageOptions()); }); WebApplicationBuilder.Services.AddHangfireServer(); WebApplicationBuilder.Logging.AddFilter( "Hangfire.Server.BackgroundServerProcess", LogLevel.Warning ); WebApplicationBuilder.Logging.AddFilter( "Hangfire.BackgroundJobServer", LogLevel.Warning ); return Task.CompletedTask; } private Task UseHangfire() { if (WebApplication.Environment.IsDevelopment()) WebApplication.UseHangfireDashboard(); return Task.CompletedTask; } #endregion }