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 96% rename from LANCommander.Server/AutoMapper.cs rename to LANCommander.Server/LANCommanderMappingProfile.cs index 9c9a201a..792f0d38 100644 --- a/LANCommander.Server/AutoMapper.cs +++ b/LANCommander.Server/LANCommanderMappingProfile.cs @@ -3,9 +3,9 @@ using LANCommander.SDK.Models; 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 3db89c97..dd8054b6 100644 --- a/LANCommander.Server/Program.cs +++ b/LANCommander.Server/Program.cs @@ -1,556 +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 LANCommander.Server.Extensions; +using Microsoft.Data.Sqlite; using Microsoft.AspNetCore.Http.Features; using LANCommander.SDK.Enums; using Serilog; -using Serilog.Sinks.AspNetCore.App.SignalR.Extensions; -using LANCommander.Server.Logging; -using LANCommander.Server.Data.Enums; -using System.Diagnostics; -using System.Net; -using LANCommander.Server.Data.Interceptors; -using LANCommander.Server.Services.Factories; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Authentication; -using LANCommander.Server.Jobs.Background; -using LANCommander.Server.Services.Models; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Data.Sqlite; -using Microsoft.OpenApi.Models; -using Microsoft.CodeAnalysis.Options; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Scalar.AspNetCore; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; -using Serilog.Filters; +using LANCommander.Server; +using LANCommander.Server.Models; -namespace LANCommander.Server +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) { - internal class Program + 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) { - static async Task Main(string[] args) - { - Log.Logger = new LoggerConfiguration() - .Filter.ByExcluding( - Matching.WithProperty("RequestPath", v => - "/api/Ping".Equals(v, StringComparison.OrdinalIgnoreCase))) - .WriteTo.Console() - .CreateBootstrapLogger(); - - Log.Information("Starting application..."); - - var builder = WebApplication.CreateBuilder(args); - - builder.Services.AddWindowsService(options => - { - options.ServiceName = "LANCommander Server"; - }); - - builder.Services.AddSystemd(); - - ConfigurationManager configuration = builder.Configuration; - - #region Debug - if (args.Contains("--debugger")) - { - var currentProcess = Process.GetCurrentProcess(); - - Console.WriteLine($"Waiting for debugger to attach... Process ID: {currentProcess.Id}"); - - while (!Debugger.IsAttached) - { - Thread.Sleep(100); - } - - Console.WriteLine("Debugger attached."); - } - #endregion - - // Add services to the container. - Log.Debug("Loading settings"); - var settings = SettingService.GetSettings(true); - - var databaseProviderParameter = args.FirstOrDefault(arg => arg.StartsWith("--database-provider="))?.Split('=', 2).Last(); - var connectionStringParameter = args.FirstOrDefault(arg => arg.StartsWith("--connection-string="))?.Split('=', 2).Last(); - - if (!String.IsNullOrWhiteSpace(databaseProviderParameter)) - DatabaseContext.Provider = Enum.Parse(databaseProviderParameter); - else - DatabaseContext.Provider = settings.DatabaseProvider; - - if (!String.IsNullOrWhiteSpace(connectionStringParameter)) - DatabaseContext.ConnectionString = connectionStringParameter; - else - DatabaseContext.ConnectionString = settings.DatabaseConnectionString; - - 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 - .Filter.ByExcluding( - Matching.WithProperty("RequestPath", v => - "/api/Ping".Equals(v, StringComparison.OrdinalIgnoreCase))) - .WriteTo.Console() - .WriteTo.File(Path.Combine(settings.Logs.StoragePath, "log-.txt"), rollingInterval: (RollingInterval)(int)settings.Logs.ArchiveEvery) -#if DEBUG - .WriteTo.Seq("http://localhost:5341") - .MinimumLevel.Debug() -#endif - .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.AddRazorComponents() - .AddInteractiveServerComponents(); - - builder.Services.AddCascadingAuthenticationState(); - - builder.Services.AddAutoMapper(typeof(AutoMapper)); - - Log.Debug("Starting web server on port {Port}", settings.Port); - builder.WebHost.ConfigureKestrel(options => - { - options.Listen(IPAddress.Any, settings.Port); - - // Configure as HTTP only - if (settings.UseSSL) - { - options.Listen(IPAddress.Any, settings.SSLPort, listenOptions => - { - listenOptions.UseHttps(settings.CertificatePath, settings.CertificatePassword); - }); - } - }); - - 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.AddHttpContextAccessor(); - builder.Services.AddSingleton(); - - builder.Services.AddDbContextFactory(); - builder.Services.AddDbContext(); - - builder.Services.AddDatabaseDeveloperPageExceptionFilter(); - - Log.Debug("Initializing Identity"); - builder.Services.AddIdentityCore((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() - .AddSignInManager() - .AddDefaultTokenProviders(); - - builder.Services.Configure(options => - { - options.Secure = settings.Authentication.CookieSecurePolicy; - options.MinimumSameSitePolicy = settings.Authentication.MinimumSameSitePolicy; - }); - - var authBuilder = builder.Services.AddAuthentication(options => - { - options.DefaultScheme = IdentityConstants.ApplicationScheme; - options.DefaultSignInScheme = IdentityConstants.ExternalScheme; - /*options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;*/ - }); - - foreach (var authenticationProvider in settings.Authentication.AuthenticationProviders) - { - try - { - switch (authenticationProvider.Type) - { - case AuthenticationProviderType.OAuth2: - authBuilder.AddOAuth(authenticationProvider); - break; - - case AuthenticationProviderType.OpenIdConnect: - authBuilder.AddOpenIdConnect(authenticationProvider); - break; - - case AuthenticationProviderType.Saml: - throw new NotImplementedException("SAML providers are not supported at this time."); - break; - } - } - catch (Exception ex) - { - Log.Error(ex, "Authentication Provider {Name} could not be registered", - authenticationProvider.Name); - } - } - - authBuilder.AddIdentityCookies(); - - builder.Services.ConfigureApplicationCookie(options => - { - options.LoginPath = "/Login"; - options.LogoutPath = "/Logout"; - options.AccessDeniedPath = "/AccessDenied"; - }); - - authBuilder.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(options => - { - options.CustomSchemaIds(type => type.ToString()); - options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme - { - In = ParameterLocation.Header, - Description = "Enter a valid access token", - Name = "Authorization", - Type = SecuritySchemeType.Http, - BearerFormat = "JWT", - Scheme = "Bearer" - }); - - options.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - } - }, - new string[]{} - } - }); - }); - - 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.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.UseHttpsRedirection(); - - app.MapHub("/hubs/gameserver"); - - app.UseForwardedHeaders(new ForwardedHeadersOptions - { - ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto - }); - - 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(); - } - 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.UseSwagger(options => - { - options.RouteTemplate = "/openapi/{documentName}.json"; - }); - app.MapScalarApiReference(); - app.UseHangfireDashboard(); - - // app.UseHttpsRedirection(); - - app.UseCookiePolicy(); - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseMvcWithDefaultRoute(); - - Log.Debug("Registering Endpoints"); - - app.MapHub("/logging"); - - app.UseAntiforgery(); - app.UseStaticFiles(); - - app.UseForwardedHeaders(new ForwardedHeadersOptions - { - ForwardedHeaders = ForwardedHeaders.XForwardedProto - }); - - app.MapRazorComponents() - .AddInteractiveServerRenderMode(); - - app.UseEndpoints(endpoints => - { - endpoints.MapFallbackToPage("/_Host"); - endpoints.MapControllers(); - }); - - Log.Debug("Ensuring required directories exist"); - - if (!Directory.Exists(settings.UserSaves.StoragePath)) - Directory.CreateDirectory(settings.UserSaves.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"); - - if (DatabaseContext.Provider != DatabaseProvider.Unknown) - { - await using var scope = app.Services.CreateAsyncScope(); - using var db = scope.ServiceProvider.GetService(); - - if ((await db.Database.GetPendingMigrationsAsync()).Any()) - { - if (DatabaseContext.Provider == DatabaseProvider.SQLite) - { - 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.GetAsync(s => s.Autostart && s.AutostartMethod == ServerAutostartMethod.OnApplicationStart)) - { - 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); - } - } - - await db.DisposeAsync(); - await scope.DisposeAsync(); - - BackgroundJob.Enqueue(x => x.ExecuteAsync()); - } - else - Log.Debug("No database provider has been setup, application is fresh and needs first time setup"); - - app.Run(); - } + 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); + } + } +}