using System.Text; using System.Text.Json; using Docker.DotNet; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http.Connections; using Microsoft.IdentityModel.Tokens; using MoonCore.EnvConfiguration; using MoonCore.Extended.Extensions; using MoonCore.Extensions; using MoonCore.Helpers; using MoonlightServers.Daemon.Configuration; using MoonlightServers.Daemon.Helpers; using MoonlightServers.Daemon.Http.Hubs; using MoonlightServers.Daemon.Services; namespace MoonlightServers.Daemon; public class Startup { private string[] Args; // Configuration private AppConfiguration Configuration; // Logging private ILoggerProvider[] LoggerProviders; private ILoggerFactory LoggerFactory; private ILogger Logger; // WebApplication Stuff private WebApplication WebApplication; private WebApplicationBuilder WebApplicationBuilder; public async Task Run(string[] args) { Args = args; await SetupStorage(); await SetupAppConfiguration(); await SetupLogging(); await CreateWebApplicationBuilder(); await ConfigureKestrel(); await RegisterAppConfiguration(); await RegisterLogging(); await RegisterBase(); await RegisterAuth(); await RegisterDocker(); await RegisterServers(); await RegisterSignalR(); await RegisterCors(); await BuildWebApplication(); await UseBase(); await UseCors(); await UseAuth(); await UseBaseMiddleware(); await MapBase(); await MapHubs(); await WebApplication.RunAsync(); } private Task SetupStorage() { Directory.CreateDirectory("storage"); Directory.CreateDirectory(PathBuilder.Dir("storage", "logs")); Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins")); return Task.CompletedTask; } #region Base private Task RegisterBase() { WebApplicationBuilder.Services.AutoAddServices(); WebApplicationBuilder.Services.AddControllers(); WebApplicationBuilder.Services.AddApiExceptionHandler(); return Task.CompletedTask; } private Task ConfigureKestrel() { WebApplicationBuilder.WebHost.ConfigureKestrel(options => { options.Limits.MaxRequestBodySize = ByteConverter.FromMegaBytes(Configuration.Files.UploadChunkSize).Bytes; }); return Task.CompletedTask; } private Task UseBase() { WebApplication.UseRouting(); WebApplication.UseApiExceptionHandler(); return Task.CompletedTask; } private Task UseBaseMiddleware() { return Task.CompletedTask; } private Task MapBase() { WebApplication.MapControllers(); return Task.CompletedTask; } #endregion #region Docker private Task RegisterDocker() { var dockerClient = new DockerClientConfiguration( new Uri(Configuration.Docker.Uri) ).CreateClient(); WebApplicationBuilder.Services.AddSingleton(dockerClient); return Task.CompletedTask; } #endregion #region Configurations private async Task SetupAppConfiguration() { var configurationBuilder = new ConfigurationBuilder(); // Ensure configuration file exists var jsonFilePath = PathBuilder.File(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: "_"); var 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() { LoggerProviders = LoggerBuildHelper.BuildFromConfiguration(configuration => { configuration.Console.Enable = true; configuration.Console.EnableAnsiMode = true; configuration.FileLogging.Enable = true; configuration.FileLogging.EnableLogRotation = true; configuration.FileLogging.Path = PathBuilder.File("storage", "logs", "WebAppTemplate.log"); configuration.FileLogging.RotateLogNameTemplate = PathBuilder.File("storage", "logs", "WebAppTemplate.log.{0}"); }); LoggerFactory = new LoggerFactory(); LoggerFactory.AddProviders(LoggerProviders); Logger = LoggerFactory.CreateLogger(); return Task.CompletedTask; } private async Task RegisterLogging() { // Configure application logging WebApplicationBuilder.Logging.ClearProviders(); WebApplicationBuilder.Logging.AddProviders(LoggerProviders); // Logging levels var logConfigPath = PathBuilder.File("storage", "logConfig.json"); // Ensure logging config, add a default one is missing if (!File.Exists(logConfigPath)) { var logLevels = new Dictionary { { "Default", "Information" }, { "Microsoft.AspNetCore", "Warning" }, { "System.Net.Http.HttpClient", "Warning" } }; var logLevelsJson = JsonSerializer.Serialize(logLevels); var logConfig = "{\"LogLevel\":" + logLevelsJson + "}"; await File.WriteAllTextAsync(logConfigPath, logConfig); } // Configure logging configuration WebApplicationBuilder.Logging.AddConfiguration( await File.ReadAllTextAsync(logConfigPath) ); // 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 Servers private Task RegisterServers() { WebApplicationBuilder.Services.AddHostedService( sp => sp.GetRequiredService() ); return Task.CompletedTask; } private Task UseServers() { return Task.CompletedTask; } #endregion #region Hubs private Task RegisterSignalR() { WebApplicationBuilder.Services.AddSignalR(); return Task.CompletedTask; } private Task MapHubs() { WebApplication.MapHub("api/servers/ws", options => { options.AllowStatefulReconnects = false; options.CloseOnAuthenticationExpiration = true; }); return Task.CompletedTask; } #endregion #region Cors private Task RegisterCors() { //TODO: IMPORTANT: CHANGE !!! WebApplicationBuilder.Services.AddCors(x => x.AddDefaultPolicy(builder => builder .SetIsOriginAllowed(_ => true) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials() ) ); return Task.CompletedTask; } private Task UseCors() { WebApplication.UseCors(); return Task.CompletedTask; } #endregion #region Authentication private Task RegisterAuth() { WebApplicationBuilder.Services .AddAuthentication("token") .AddScheme("token", options => { options.Token = Configuration.Security.Token; }) .AddJwtBearer("accessToken", options => { options.TokenValidationParameters = new TokenValidationParameters() { ClockSkew = TimeSpan.Zero, ValidateAudience = true, ValidateIssuer = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidateActor = false, IssuerSigningKeys = [ new SymmetricSecurityKey( Encoding.UTF8.GetBytes(Configuration.Security.Token) ) ], AudienceValidator = (audiences, _, _) => audiences.Contains(Configuration.Security.TokenId) }; // Some requests like the signal r websockets or the upload and download endpoints require the user to provide // the access token via the query instead of the header field due to client limitations. To reuse the authentication // on all these requests as we do for all other endpoints which require an access token, we are defining // the custom behaviour to load the access token from the query for these specific endpoints below options.Events = new JwtBearerEvents() { OnMessageReceived = context => { if ( !context.HttpContext.Request.Path.StartsWithSegments("/api/servers/ws") && !context.HttpContext.Request.Path.StartsWithSegments("/api/servers/upload") && !context.HttpContext.Request.Path.StartsWithSegments("/api/servers/download") ) { return Task.CompletedTask; } var accessToken = context.Request.Query["access_token"]; if (string.IsNullOrEmpty(accessToken)) return Task.CompletedTask; context.Token = accessToken; return Task.CompletedTask; } }; }); WebApplicationBuilder.Services.AddAuthorization(options => { // We are defining the access token policies here. Because the same jwt secret is used by the panel // to generate jwt access tokens for all sorts of daemon related stuff we need to separate // the type of access token using the type parameter provided in the claims. options.AddPolicy("serverWebsocket", builder => { builder.RequireClaim("type", "websocket"); }); options.AddPolicy("serverUpload", builder => { builder.RequireClaim("type", "upload"); }); options.AddPolicy("serverDownload", builder => { builder.RequireClaim("type", "download"); }); }); return Task.CompletedTask; } private Task UseAuth() { WebApplication.UseAuthentication(); WebApplication.UseAuthorization(); return Task.CompletedTask; } #endregion }