From 8e5a4f6bf43a3de5002a403cc166c18095d83532 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 2 Jan 2025 11:40:15 +1100 Subject: [PATCH] Refactoring startup for easier understanding and tracing Broke the startup of the app into a series of extension methods (and classes to contain them), rather than one long method. This means the stack tracers are easier to follow when the startup fails, and it's clearer what's happening when throughout the pipeline. I've included a few optimisations, such as starting to rely more on DI for injecting things (like the logger) rather than the static logger, which in turn can help with testability and evaluation of the callstack. --- .../Extensions/AspNetExtensions.cs | 79 +++ .../Extensions/EndpointExtensions.cs | 30 + .../Extensions/IdentityExtensions.cs | 50 ++ .../Extensions/LoggerExtensions.cs | 29 + .../Extensions/ServiceExtensions.cs | 80 +++ ...apper.cs => LANCommanderMappingProfile.cs} | 4 +- LANCommander.Server/Program.cs | 567 ++++++------------ .../Properties/launchSettings.json | 17 +- 8 files changed, 469 insertions(+), 387 deletions(-) create mode 100644 LANCommander.Server/Extensions/AspNetExtensions.cs create mode 100644 LANCommander.Server/Extensions/EndpointExtensions.cs create mode 100644 LANCommander.Server/Extensions/IdentityExtensions.cs create mode 100644 LANCommander.Server/Extensions/LoggerExtensions.cs create mode 100644 LANCommander.Server/Extensions/ServiceExtensions.cs rename LANCommander.Server/{AutoMapper.cs => LANCommanderMappingProfile.cs} (95%) diff --git a/LANCommander.Server/Extensions/AspNetExtensions.cs b/LANCommander.Server/Extensions/AspNetExtensions.cs new file mode 100644 index 00000000..b55d5154 --- /dev/null +++ b/LANCommander.Server/Extensions/AspNetExtensions.cs @@ -0,0 +1,79 @@ +using Serilog; + +namespace LANCommander.Server; + +public static class AspNetExtensions +{ + public static void AddRazor(this WebApplicationBuilder builder) + { + Log.Debug("Configuring MVC and Blazor"); + builder.Services + .AddMvc(static options => options.EnableEndpointRouting = false) + .AddRazorOptions(static options => + { + options.ViewLocationFormats.Clear(); + options.ViewLocationFormats.Add("/UI/Views/{1}/{0}.cshtml"); + options.ViewLocationFormats.Add("/UI/Views/Shared/{0}.cshtml"); + options.ViewLocationFormats.Add("/UI/Pages/Shared/{0}.cshtml"); + + options.AreaViewLocationFormats.Clear(); + options.AreaViewLocationFormats.Add("/Areas/{2}/Views/{1}/{0}.cshtml"); + options.AreaViewLocationFormats.Add("/Areas/{2}/Views/Shared/{0}.cshtml"); + options.AreaViewLocationFormats.Add("/UI/Views/Shared/{0}.cshtml"); + options.AreaViewLocationFormats.Add("/UI/Pages/Shared/{0}.cshtml"); + + options.PageViewLocationFormats.Clear(); + options.PageViewLocationFormats.Add("/UI/Pages/{1}/{0}.cshtml"); + options.PageViewLocationFormats.Add("/UI/Pages/Shared/{0}.cshtml"); + options.PageViewLocationFormats.Add("/UI/Views/Shared/{0}.cshtml"); + + options.AreaPageViewLocationFormats.Clear(); + options.AreaPageViewLocationFormats.Add("/Areas/{2}/Pages/{1}/{0}.cshtml"); + options.AreaPageViewLocationFormats.Add("/Areas/{2}/Pages/Shared/{0}.cshtml"); + options.AreaPageViewLocationFormats.Add("/Areas/{2}/Views/Shared/{0}.cshtml"); + options.AreaPageViewLocationFormats.Add("/UI/Pages/Shared/{0}.cshtml"); + options.AreaPageViewLocationFormats.Add("/UI/Views/Shared/{0}.cshtml"); + }); + + builder.Services.AddRazorPages(static options => options.RootDirectory = "/UI/Pages"); + + builder.Services + .AddServerSideBlazor() + .AddCircuitOptions(static option => option.DetailedErrors = true) + .AddHubOptions(static option => + { + option.MaximumReceiveMessageSize = 1024 * 1024 * 11; + option.DisableImplicitFromServicesParameters = true; + }); + } + + public static void AddSignalR(this WebApplicationBuilder builder) + { + builder.Services.AddSignalR().AddJsonProtocol(static options => + { + options.PayloadSerializerOptions.PropertyNamingPolicy = null; + }); + } + + public static void AddCors(this WebApplicationBuilder builder) + { + builder.Services.AddCors(static options => + options.AddPolicy("CorsPolicy", static builder => + { + builder.AllowAnyHeader() + .AllowAnyMethod() + .SetIsOriginAllowed(static (host) => true) + .AllowCredentials(); + }) + ); + } + + public static void AddControllers(this WebApplicationBuilder builder) + { + Log.Debug("Initializing Controllers"); + builder.Services.AddControllers().AddJsonOptions(static x => + { + x.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles; + }); + } +} \ No newline at end of file diff --git a/LANCommander.Server/Extensions/EndpointExtensions.cs b/LANCommander.Server/Extensions/EndpointExtensions.cs new file mode 100644 index 00000000..d0bb4ab2 --- /dev/null +++ b/LANCommander.Server/Extensions/EndpointExtensions.cs @@ -0,0 +1,30 @@ +using LANCommander.Server.Services; + +namespace LANCommander.Server; + +public static class EndpointExtensions +{ + public static void UseRobots(this WebApplication app) => + app.Use(async (context, next) => + { + if (context.Request.Path.StartsWithSegments("/robots.txt")) + { + context.Response.ContentType = "text/plain"; + + await context.Response.WriteAsync("User-agent: *\nDisallow: /Identity/"); + return; + } + + await next(); + }); + + public static void UseApiVersioning(this WebApplication app) => + app.Use((context, next) => + { + var headers = context.Response.Headers; + + headers.Append("X-API-Version", UpdateService.GetCurrentVersion().ToString()); + + return next(); + }); +} diff --git a/LANCommander.Server/Extensions/IdentityExtensions.cs b/LANCommander.Server/Extensions/IdentityExtensions.cs new file mode 100644 index 00000000..697516a7 --- /dev/null +++ b/LANCommander.Server/Extensions/IdentityExtensions.cs @@ -0,0 +1,50 @@ +using LANCommander.Server.Data.Models; +using LANCommander.Server.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using Serilog; +using System.Text; + +namespace LANCommander.Server; + +public static class IdentityExtensions +{ + public static void AddIdentity(this WebApplicationBuilder builder, LANCommanderSettings settings) + { + Log.Debug("Initializing Identity"); + builder.Services.AddDefaultIdentity((IdentityOptions options) => + { + options.SignIn.RequireConfirmedAccount = false; + options.SignIn.RequireConfirmedEmail = false; + + options.Password.RequireNonAlphanumeric = settings.Authentication.PasswordRequireNonAlphanumeric; + options.Password.RequireLowercase = settings.Authentication.PasswordRequireLowercase; + options.Password.RequireUppercase = settings.Authentication.PasswordRequireUppercase; + options.Password.RequireDigit = settings.Authentication.PasswordRequireDigit; + options.Password.RequiredLength = settings.Authentication.PasswordRequiredLength; + }) + .AddRoles() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + builder.Services.AddAuthentication(options => + { + /*options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;*/ + }) + .AddJwtBearer(options => + { + options.SaveToken = true; + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = new TokenValidationParameters() + { + ValidateIssuer = false, + ValidateAudience = false, + // ValidAudience = configuration["JWT:ValidAudience"], + // ValidIssuer = configuration["JWT:ValidIssuer"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.Authentication.TokenSecret)) + }; + }); + } + } diff --git a/LANCommander.Server/Extensions/LoggerExtensions.cs b/LANCommander.Server/Extensions/LoggerExtensions.cs new file mode 100644 index 00000000..7f57ea69 --- /dev/null +++ b/LANCommander.Server/Extensions/LoggerExtensions.cs @@ -0,0 +1,29 @@ +using LANCommander.Server.Hubs; +using Serilog.Sinks.AspNetCore.App.SignalR.Extensions; +using Serilog; +using LANCommander.Server.Models; + +namespace LANCommander.Server; + +public static class LoggerExtensions +{ + public static void AddLogger(this WebApplicationBuilder builder) + { + Log.Logger = new LoggerConfiguration() + .WriteTo.Console() + .CreateBootstrapLogger(); + + builder.Services.AddSerilogHub(); + builder.Services.AddSerilog((serviceProvider, config) => + { + var settings = serviceProvider.GetRequiredService(); + config + .WriteTo.Console() + .WriteTo.File(Path.Combine(settings.Logs.StoragePath, "log-.txt"), rollingInterval: settings.Logs.ArchiveEvery) + .WriteTo.SignalR( + serviceProvider, + (context, message, logEvent) => LoggingHub.Log(context, message, logEvent) + ); + }); + } +} diff --git a/LANCommander.Server/Extensions/ServiceExtensions.cs b/LANCommander.Server/Extensions/ServiceExtensions.cs new file mode 100644 index 00000000..0be3dbf3 --- /dev/null +++ b/LANCommander.Server/Extensions/ServiceExtensions.cs @@ -0,0 +1,80 @@ +using LANCommander.SDK; +using LANCommander.Server.Models; +using LANCommander.Server.Services.MediaGrabbers; +using LANCommander.Server.Services; +using Serilog; +using Hangfire; +using LANCommander.Server.Data; +using Microsoft.EntityFrameworkCore; + +namespace LANCommander.Server; + +public static class ServiceExtensions +{ + public static void AddLANCommanderServices(this WebApplicationBuilder builder, LANCommanderSettings settings) + { + Log.Debug("Registering services"); + builder.Services.AddSingleton(new Client("", "")); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + if (settings.Beacon.Enabled) + { + Log.Debug("The beacons have been lit! LANCommander calls for players!"); + builder.Services.AddHostedService(); + } + } + + public static void AddHangfire(this WebApplicationBuilder builder) + { + builder.Services.AddHangfire(static (sp, configuration) => + { + var logger = sp.GetRequiredService>(); + logger.LogDebug("Initializing Hangfire"); + configuration + .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseInMemoryStorage(); + }); + builder.Services.AddHangfireServer(); + } + + public static void AddDatabase(this WebApplicationBuilder builder) + { + builder.Services.AddDbContext(static (sp, b) => + { + var logger = sp.GetRequiredService>(); + var settings = sp.GetRequiredService(); + logger.LogDebug("Initializing DatabaseContext with connection string {ConnectionString}", settings.DatabaseConnectionString); + b.UseLazyLoadingProxies(); + b.UseSqlite(settings.DatabaseConnectionString); + }); + builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + } +} diff --git a/LANCommander.Server/AutoMapper.cs b/LANCommander.Server/LANCommanderMappingProfile.cs similarity index 95% rename from LANCommander.Server/AutoMapper.cs rename to LANCommander.Server/LANCommanderMappingProfile.cs index ecbd8616..0aaea0cd 100644 --- a/LANCommander.Server/AutoMapper.cs +++ b/LANCommander.Server/LANCommanderMappingProfile.cs @@ -2,9 +2,9 @@ namespace LANCommander.Server { - public class AutoMapper : Profile + public class LANCommanderMappingProfile : Profile { - public AutoMapper() + public LANCommanderMappingProfile() { CreateMap(); CreateMap(); diff --git a/LANCommander.Server/Program.cs b/LANCommander.Server/Program.cs index 70ee8161..db9210d8 100644 --- a/LANCommander.Server/Program.cs +++ b/LANCommander.Server/Program.cs @@ -1,378 +1,207 @@ using LANCommander.Server.Data; -using LANCommander.Server.Data.Models; using LANCommander.Server.Hubs; using LANCommander.Server.Services; -using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Tokens; -using System.Text; using Hangfire; -using LANCommander.Server.Services.MediaGrabbers; using Microsoft.Data.Sqlite; -using LANCommander.Server.Extensions; using Microsoft.AspNetCore.Http.Features; using LANCommander.SDK.Enums; using Serilog; -using Serilog.Sinks.AspNetCore.App.SignalR.Extensions; -using LANCommander.Server.Logging; - -namespace LANCommander.Server -{ - internal class Program - { - static async Task Main(string[] args) - { - Log.Logger = new LoggerConfiguration() - .WriteTo.Console() - .CreateBootstrapLogger(); - - var builder = WebApplication.CreateBuilder(args); - - ConfigurationManager configuration = builder.Configuration; - - // Add services to the container. - Log.Debug("Loading settings"); - var settings = SettingService.GetSettings(true); - Log.Debug("Loaded!"); - - - Log.Debug("Configuring logging"); - - builder.Services.AddSignalR().AddJsonProtocol(options => - { - options.PayloadSerializerOptions.PropertyNamingPolicy = null; - }); - - builder.Services.AddSerilogHub(); - builder.Services.AddSerilog((serviceProvider, config) => config - .WriteTo.Console() - .WriteTo.File(Path.Combine(settings.Logs.StoragePath, "log-.txt"), rollingInterval: settings.Logs.ArchiveEvery) - .WriteTo.SignalR( - serviceProvider, - (context, message, logEvent) => LoggingHub.Log(context, message, logEvent) - )); - - #region Validate Settings - Log.Debug("Validating settings"); - if (settings?.Authentication?.TokenSecret?.Length < 16) - { - Log.Debug("JWT token secret is too short. Regenerating..."); - settings.Authentication.TokenSecret = Guid.NewGuid().ToString(); - SettingService.SaveSettings(settings); - } - Log.Debug("Done validating settings"); - #endregion - - Log.Debug("Configuring MVC and Blazor"); - builder.Services - .AddMvc(options => options.EnableEndpointRouting = false) - .AddRazorOptions(options => - { - options.ViewLocationFormats.Clear(); - options.ViewLocationFormats.Add("/UI/Views/{1}/{0}.cshtml"); - options.ViewLocationFormats.Add("/UI/Views/Shared/{0}.cshtml"); - options.ViewLocationFormats.Add("/UI/Pages/Shared/{0}.cshtml"); - - options.AreaViewLocationFormats.Clear(); - options.AreaViewLocationFormats.Add("/Areas/{2}/Views/{1}/{0}.cshtml"); - options.AreaViewLocationFormats.Add("/Areas/{2}/Views/Shared/{0}.cshtml"); - options.AreaViewLocationFormats.Add("/UI/Views/Shared/{0}.cshtml"); - options.AreaViewLocationFormats.Add("/UI/Pages/Shared/{0}.cshtml"); - - options.PageViewLocationFormats.Clear(); - options.PageViewLocationFormats.Add("/UI/Pages/{1}/{0}.cshtml"); - options.PageViewLocationFormats.Add("/UI/Pages/Shared/{0}.cshtml"); - options.PageViewLocationFormats.Add("/UI/Views/Shared/{0}.cshtml"); - - options.AreaPageViewLocationFormats.Clear(); - options.AreaPageViewLocationFormats.Add("/Areas/{2}/Pages/{1}/{0}.cshtml"); - options.AreaPageViewLocationFormats.Add("/Areas/{2}/Pages/Shared/{0}.cshtml"); - options.AreaPageViewLocationFormats.Add("/Areas/{2}/Views/Shared/{0}.cshtml"); - options.AreaPageViewLocationFormats.Add("/UI/Pages/Shared/{0}.cshtml"); - options.AreaPageViewLocationFormats.Add("/UI/Views/Shared/{0}.cshtml"); - }); - - builder.Services.AddRazorPages(options => - { - options.RootDirectory = "/UI/Pages"; - }); - - builder.Services.AddServerSideBlazor().AddCircuitOptions(option => - { - option.DetailedErrors = true; - }).AddHubOptions(option => - { - option.MaximumReceiveMessageSize = 1024 * 1024 * 11; - option.DisableImplicitFromServicesParameters = true; - }); - - builder.Services.AddAutoMapper(typeof(AutoMapper)); - - Log.Debug("Starting web server on port {Port}", settings.Port); - builder.WebHost.ConfigureKestrel(options => - { - // Configure as HTTP only - options.ListenAnyIP(settings.Port); - }); - - builder.Services.AddCors(options => options.AddPolicy("CorsPolicy", builder => - { - builder.AllowAnyHeader() - .AllowAnyMethod() - .SetIsOriginAllowed((host) => true) - .AllowCredentials(); - })); - - Log.Debug("Initializing DatabaseContext with connection string {ConnectionString}", settings.DatabaseConnectionString); - builder.Services.AddDbContext(b => - { - b.UseLazyLoadingProxies(); - b.UseSqlite(settings.DatabaseConnectionString); - }); - - builder.Services.AddDatabaseDeveloperPageExceptionFilter(); - - Log.Debug("Initializing Identity"); - builder.Services.AddDefaultIdentity((IdentityOptions options) => - { - options.SignIn.RequireConfirmedAccount = false; - options.SignIn.RequireConfirmedEmail = false; - - options.Password.RequireNonAlphanumeric = settings.Authentication.PasswordRequireNonAlphanumeric; - options.Password.RequireLowercase = settings.Authentication.PasswordRequireLowercase; - options.Password.RequireUppercase = settings.Authentication.PasswordRequireUppercase; - options.Password.RequireDigit = settings.Authentication.PasswordRequireDigit; - options.Password.RequiredLength = settings.Authentication.PasswordRequiredLength; - }) - .AddRoles() - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); - - builder.Services.AddAuthentication(options => - { - /*options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;*/ - }) - .AddJwtBearer(options => - { - options.SaveToken = true; - options.RequireHttpsMetadata = false; - options.TokenValidationParameters = new TokenValidationParameters() - { - ValidateIssuer = false, - ValidateAudience = false, - // ValidAudience = configuration["JWT:ValidAudience"], - // ValidIssuer = configuration["JWT:ValidIssuer"], - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.Authentication.TokenSecret)) - }; - }); - - Log.Debug("Initializing Controllers"); - builder.Services.AddControllers().AddJsonOptions(x => - { - x.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles; - }); - - Log.Debug("Initializing Hangfire"); - builder.Services.AddHangfire(configuration => - configuration - .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) - .UseSimpleAssemblyNameTypeSerializer() - .UseRecommendedSerializerSettings() - .UseInMemoryStorage()); - builder.Services.AddHangfireServer(); - - builder.Services.AddFusionCache(); - - Log.Debug("Registering Swashbuckle"); - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); - - Log.Debug("Registering AntDesign Blazor"); - builder.Services.AddAntDesign(); - - builder.Services.AddHttpClient(); - - Log.Debug("Registering Services"); - builder.Services.AddSingleton(new SDK.Client("", "")); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - if (settings.Beacon?.Enabled ?? false) - { - Log.Debug("The beacons have been lit! LANCommander calls for players!"); - builder.Services.AddHostedService(); - } - - builder.WebHost.UseStaticWebAssets(); - - builder.WebHost.UseKestrel(options => - { - options.Limits.MaxRequestBodySize = long.MaxValue; - options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(5); - }); - - builder.Services.Configure(options => - { - options.MultipartBodyLengthLimit = long.MaxValue; - }); - - Log.Debug("Building Application"); - var app = builder.Build(); - - app.UseCors("CorsPolicy"); - - app.MapHub("/hubs/gameserver"); - - app.Use(async (context, next) => - { - if (context.Request.Path.StartsWithSegments("/robots.txt")) - { - context.Response.ContentType = "text/plain"; - - await context.Response.WriteAsync("User-agent: *\nDisallow: /Identity/"); - } - else await next(); - }); - - app.Use((context, next) => - { - var headers = context.Response.Headers; - - headers.Append("X-API-Version", UpdateService.GetCurrentVersion().ToString()); - - return next(); - }); - - // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) - { - Log.Debug("App has been run in a development environment"); - app.UseMigrationsEndPoint(); - app.UseSwagger(); - app.UseSwaggerUI(); - } - else - { - app.UseExceptionHandler("/Home/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - app.UseHangfireDashboard(); - - // app.UseHttpsRedirection(); - app.UseStaticFiles(); - - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseMvcWithDefaultRoute(); - - Log.Debug("Registering Endpoints"); - - app.MapHub("/logging"); - - app.UseEndpoints(endpoints => - { - endpoints.MapBlazorHub(); - endpoints.MapFallbackToPage("/_Host"); - endpoints.MapControllers(); - }); - - Log.Debug("Ensuring required directories exist"); - if (!Directory.Exists(settings.Archives.StoragePath)) - Directory.CreateDirectory(settings.Archives.StoragePath); - - if (!Directory.Exists(settings.UserSaves.StoragePath)) - Directory.CreateDirectory(settings.UserSaves.StoragePath); - - if (!Directory.Exists(settings.Media.StoragePath)) - Directory.CreateDirectory(settings.Media.StoragePath); - - if (!Directory.Exists(settings.Update.StoragePath)) - Directory.CreateDirectory(settings.Update.StoragePath); - - if (!Directory.Exists("Snippets")) - Directory.CreateDirectory("Snippets"); - - if (!Directory.Exists("Backups")) - Directory.CreateDirectory("Backups"); - - // Migrate - Log.Debug("Migrating database if required"); - await using var scope = app.Services.CreateAsyncScope(); - using var db = scope.ServiceProvider.GetService(); - - if ((await db.Database.GetPendingMigrationsAsync()).Any()) - { - var dataSource = new SqliteConnectionStringBuilder(settings.DatabaseConnectionString).DataSource; - - var backupName = Path.Combine("Backups", $"LANCommander.db.{DateTime.Now.ToString("dd-MM-yyyy-HH.mm.ss.bak")}"); - - if (File.Exists(dataSource)) - { - Log.Information("Migrations pending, database will be backed up to {BackupName}", backupName); - File.Copy(dataSource, backupName); - } - - await db.Database.MigrateAsync(); - } - else - Log.Debug("No pending migrations are available. Skipping database migration."); - - // Autostart any server processes - Log.Debug("Autostarting Servers"); - var serverService = scope.ServiceProvider.GetService(); - var serverProcessService = scope.ServiceProvider.GetService(); - - foreach (var server in await serverService.Get(s => s.Autostart && s.AutostartMethod == ServerAutostartMethod.OnApplicationStart).ToListAsync()) - { - try - { - Log.Debug("Autostarting server {ServerName} with a delay of {AutostartDelay} seconds", server.Name, server.AutostartDelay); - - if (server.AutostartDelay > 0) - await Task.Delay(server.AutostartDelay); - - serverProcessService.StartServerAsync(server.Id); - } - catch (Exception ex) - { - Log.Debug(ex, "An unexpected error occurred while trying to autostart the server {ServerName}", server.Name); - } - } - - app.Run(); - } - } -} - +using LANCommander.Server; +using LANCommander.Server.Models; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddLogger(); + +// Add services to the container. +Log.Debug("Loading settings"); +LANCommanderSettings settings = SettingService.GetSettings(true); +builder.Services.AddSingleton(settings); +Log.Debug("Validating settings"); +if (settings.Authentication.TokenSecret.Length < 16) +{ + Log.Debug("JWT token secret is too short. Regenerating..."); + settings.Authentication.TokenSecret = Guid.NewGuid().ToString(); + SettingService.SaveSettings(settings); +} +Log.Debug("Done validating settings"); + +builder.AddRazor(); +builder.AddSignalR(); +builder.AddCors(); +builder.AddControllers(); + +builder.Services.AddAutoMapper(typeof(LANCommanderMappingProfile)); + +builder.WebHost.ConfigureKestrel((ctx, options) => +{ + var settings = options.ApplicationServices.GetRequiredService(); + var logger = options.ApplicationServices.GetRequiredService>(); + logger.LogDebug("Starting web server on port {Port}", settings.Port); + // Configure as HTTP only + options.ListenAnyIP(settings.Port); + + options.Limits.MaxRequestBodySize = long.MaxValue; + options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(5); +}).UseKestrel(); + +builder.AddIdentity(settings); + +builder.AddHangfire(); + +builder.Services.AddFusionCache(); + +Log.Debug("Registering Swashbuckle"); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +Log.Debug("Registering AntDesign Blazor"); +builder.Services.AddAntDesign(); + +builder.Services.AddHttpClient(); + +builder.WebHost.UseStaticWebAssets(); + +builder.AddLANCommanderServices(settings); +builder.AddDatabase(); + +builder.Services.Configure(options => +{ + options.MultipartBodyLengthLimit = long.MaxValue; +}); + +Log.Debug("Building Application"); +var app = builder.Build(); + +app.UseCors("CorsPolicy"); + +app.MapHub("/hubs/gameserver"); + +app.UseRobots(); +app.UseApiVersioning(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + Log.Debug("App has been run in a development environment"); + app.UseMigrationsEndPoint(); + app.UseSwagger(); + app.UseSwaggerUI(); +} +else +{ + app.UseExceptionHandler("/Home/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHangfireDashboard(); + +// app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.UseMvcWithDefaultRoute(); + +Log.Debug("Registering Endpoints"); + +app.MapHub("/logging"); + +app.UseEndpoints(endpoints => +{ + endpoints.MapBlazorHub(); + endpoints.MapFallbackToPage("/_Host"); + endpoints.MapControllers(); +}); + +PrepareDirectories(app); + +await EnsureDatabase(app); + +await InitializeServerProcesses(app); + +app.Run(); + +static void PrepareDirectories(WebApplication app) +{ + var settings = app.Services.GetRequiredService(); + var logger = app.Services.GetRequiredService>(); + logger.LogDebug("Ensuring required directories exist"); + + IEnumerable directories = [ + settings.Logs.StoragePath, + settings.Archives.StoragePath, + settings.UserSaves.StoragePath, + settings.Media.StoragePath, + settings.Update.StoragePath, + "Snippets", + "Backups" + ]; + + foreach (var directory in directories) + { + logger.LogDebug("Ensuring directory {Directory} exists", directory); + if (!Directory.Exists(directory)) + Directory.CreateDirectory(directory); + } +} + +static async Task EnsureDatabase(WebApplication app) +{ + // Migrate + using var scope = app.Services.CreateAsyncScope(); + using var db = scope.ServiceProvider.GetRequiredService(); + var logger = scope.ServiceProvider.GetRequiredService>(); + var settings = scope.ServiceProvider.GetRequiredService(); + logger.LogDebug("Migrating database if required"); + + if (!(await db.Database.GetPendingMigrationsAsync()).Any()) + { + logger.LogDebug("No pending migrations are available. Skipping database migration."); + return; + } + + var dataSource = new SqliteConnectionStringBuilder(settings.DatabaseConnectionString).DataSource; + + var backupName = Path.Combine("Backups", $"LANCommander.db.{DateTime.Now:dd-MM-yyyy-HH.mm.ss.bak}"); + + if (File.Exists(dataSource)) + { + logger.LogInformation("Migrations pending, database will be backed up to {BackupName}", backupName); + File.Copy(dataSource, backupName); + } + + await db.Database.MigrateAsync(); +} + +static async Task InitializeServerProcesses(WebApplication app) +{ + // Autostart any server processes + using var scope = app.Services.CreateScope(); + var serverService = scope.ServiceProvider.GetRequiredService(); + var serverProcessService = scope.ServiceProvider.GetRequiredService(); + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.LogDebug("Autostarting Servers"); + + foreach (var server in await serverService.Get(s => s.Autostart && s.AutostartMethod == ServerAutostartMethod.OnApplicationStart).ToListAsync()) + { + try + { + logger.LogDebug("Autostarting server {ServerName} with a delay of {AutostartDelay} seconds", server.Name, server.AutostartDelay); + + if (server.AutostartDelay > 0) + await Task.Delay(server.AutostartDelay); + + await serverProcessService.StartServerAsync(server.Id); + } + catch (Exception ex) + { + logger.LogError(ex, "An unexpected error occurred while trying to autostart the server {ServerName}", server.Name); + } + } +} \ No newline at end of file diff --git a/LANCommander.Server/Properties/launchSettings.json b/LANCommander.Server/Properties/launchSettings.json index fba57259..02bc6d8e 100644 --- a/LANCommander.Server/Properties/launchSettings.json +++ b/LANCommander.Server/Properties/launchSettings.json @@ -1,25 +1,10 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:19829", - "sslPort": 44328 - } - }, "profiles": { "LANCommander": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:7087;http://localhost:5087", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, + "applicationUrl": "http://localhost:1337", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }