Clean up startup

This commit is contained in:
Pat Hartl 2025-03-11 21:21:16 -05:00
parent 68869d0bc8
commit 444bc9d4bc
20 changed files with 764 additions and 626 deletions

View file

@ -1,113 +0,0 @@
using System.Net;
using LANCommander.Server.Services.Models;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.OpenApi.Models;
using Serilog;
namespace LANCommander.Server;
public static class AspNetExtensions
{
public static void AddRazor(this WebApplicationBuilder builder, Settings settings)
{
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
.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAntiforgery(options =>
{
options.Cookie.SameSite = settings.Authentication.MinimumSameSitePolicy;
options.Cookie.SecurePolicy = settings.Authentication.CookieSecurePolicy;
});
}
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;
});
}
public static void ConfigureKestrel(this WebApplicationBuilder builder)
{
builder.WebHost.ConfigureKestrel(options =>
{
var settings = options.ApplicationServices.GetRequiredService<Settings>();
options.Limits.MaxRequestBodySize = long.MaxValue;
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(5);
options.Listen(IPAddress.Any, settings.Port);
if (settings.UseSSL)
{
options.Listen(IPAddress.Any, settings.SSLPort, listenOptions =>
{
listenOptions.UseHttps(settings.CertificatePath, settings.CertificatePassword);
});
}
});
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = long.MaxValue;
});
builder.WebHost.UseStaticWebAssets();
}
}

View file

@ -1,30 +1,13 @@
using System.Diagnostics;
using LANCommander.Server.Data;
using LANCommander.Server.Hubs;
using LANCommander.Server.Services;
using Microsoft.EntityFrameworkCore;
using Hangfire;
using Microsoft.Data.Sqlite;
using Microsoft.AspNetCore.Http.Features;
using LANCommander.SDK.Enums;
using Serilog;
using LANCommander.Server;
using LANCommander.Server.Data.Enums;
using LANCommander.Server.Data.Models;
using LANCommander.Server.Endpoints;
using LANCommander.Server.Jobs.Background;
using LANCommander.Server.Models;
using LANCommander.Server.Services.Models;
using LANCommander.Server.UI;
using Microsoft.AspNetCore.HttpOverrides;
using Scalar.AspNetCore;
using LANCommander.Server.Services.Importers;
using LANCommander.Server.Startup;
var builder = WebApplication.CreateBuilder(args);
if (args.Contains("--debugger"))
WaitForDebugger();
builder.WaitForDebugger();
if (args.Contains("--docker"))
SettingService.WorkingDirectory = "/app/config";
@ -32,42 +15,16 @@ if (args.Contains("--docker"))
builder.AddAsService();
builder.AddLogger();
// Add services to the container.
Log.Debug("Loading settings");
Settings settings;
if (!File.Exists(SettingService.SettingsFile))
{
var workingDirectory = Path.GetDirectoryName(SettingService.SettingsFile);
if (!String.IsNullOrWhiteSpace(workingDirectory))
Directory.CreateDirectory(workingDirectory);
settings = new Settings();
SettingService.SaveSettings(settings);
}
else
settings = SettingService.GetSettings(true);
builder.AddSettings(out 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.Services.AddSingleton(settings);
ConfigureDatabaseProvider(settings, args);
builder.AddRazor(settings);
builder.AddRazor();
builder.AddSignalR();
builder.AddCors();
builder.AddControllers();
builder.ConfigureKestrel();
builder.ConfigureAuthentication(settings);
builder.AddIdentity(settings);
builder.AddHangfire();
builder.AddOpenApi();
@ -77,19 +34,12 @@ builder.AddDatabase();
Log.Debug("Building Application");
var app = builder.Build();
app.UseDatabase(args);
app.UseCors("CorsPolicy");
app.UseHttpsRedirection();
app.UseMiddleware<RobotsMiddleware>();
app.UseMiddleware<ApiVersioningMiddleware>();
app.UseMiddleware<PingMiddleware>();
app.MapHub<GameServerHub>("/hubs/gameserver");
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseMiddlewares();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
@ -105,7 +55,7 @@ else
app.UseHsts();
}
app.UseHangfireDashboard();
app.UseHangfire();
// app.UseHttpsRedirection();
app.UseStaticFiles();
@ -117,185 +67,22 @@ app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.UseMvcWithDefaultRoute();
Log.Debug("Registering Endpoints");
app.MapHub<LoggingHub>("/logging");
app.UseSignalR();
app.UseStaticFiles();
app.MapScalar();
app.UseEndpoints(endpoints =>
{
endpoints.MapDownloadEndpoints();
endpoints.MapSaveEndpoints();
endpoints.MapControllers();
endpoints.MapFallbackToPage("/_Host");
});
app.MapEndpoints();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
PrepareDirectories(app);
app.PrepareDirectories();
await EnsureDatabase(app);
await app.MigrateDatabaseAsync();
await app.StartServerProcessesAsync();
await InitializeServerProcesses(app);
BackgroundJob.Enqueue<GenerateThumbnailsJob>(x => x.ExecuteAsync());
app.GenerateThumbnails();
app.Run();
static void WaitForDebugger()
{
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.");
}
static void ConfigureDatabaseProvider(Settings settings, string[] args)
{
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<DatabaseProvider>(databaseProviderParameter);
else
DatabaseContext.Provider = settings.DatabaseProvider;
if (!String.IsNullOrWhiteSpace(connectionStringParameter))
DatabaseContext.ConnectionString = connectionStringParameter;
else
DatabaseContext.ConnectionString = settings.DatabaseConnectionString;
}
static void PrepareDirectories(WebApplication app)
{
var settings = app.Services.GetRequiredService<Settings>();
var logger = app.Services.GetRequiredService<ILogger<Program>>();
logger.LogDebug("Ensuring required directories exist");
IEnumerable<string> directories = [
settings.Update.StoragePath,
settings.Launcher.StoragePath,
settings.Backups.StoragePath,
"Snippets",
];
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
if (DatabaseContext.Provider != DatabaseProvider.Unknown)
{
using var scope = app.Services.CreateAsyncScope();
using var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
var settings = scope.ServiceProvider.GetRequiredService<Settings>();
logger.LogDebug("Migrating database if required");
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();
var storageLocationService = scope.ServiceProvider.GetRequiredService<StorageLocationService>();
var archiveLocation = await storageLocationService.AddMissingAsync(l => l.Type == StorageLocationType.Archive && l.Default, new StorageLocation
{
Path = "Uploads",
Type = StorageLocationType.Archive,
Default = true,
});
if (!Directory.Exists(archiveLocation.Value.Path))
Directory.CreateDirectory(archiveLocation.Value.Path);
var mediaLocation = await storageLocationService.AddMissingAsync(l => l.Type == StorageLocationType.Media && l.Default, new StorageLocation
{
Path = "Media",
Type = StorageLocationType.Media,
Default = true,
});
if (!Directory.Exists(mediaLocation.Value.Path))
Directory.CreateDirectory(mediaLocation.Value.Path);
var saveLocation = await storageLocationService.AddMissingAsync(l => l.Type == StorageLocationType.Save && l.Default, new StorageLocation
{
Path = "Saves",
Type = StorageLocationType.Save,
Default = true,
});
if (!Directory.Exists(saveLocation.Value.Path))
Directory.CreateDirectory(saveLocation.Value.Path);
}
else
logger.LogDebug("No pending migrations are available. Skipping database migration.");
}
}
static async Task InitializeServerProcesses(WebApplication app)
{
if (DatabaseContext.Provider != DatabaseProvider.Unknown)
{
// Autostart any server processes
using var scope = app.Services.CreateScope();
var serverService = scope.ServiceProvider.GetRequiredService<ServerService>();
var serverProcessService = scope.ServiceProvider.GetRequiredService<ServerProcessService>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogDebug("Autostarting Servers");
// Autostart IPX relay
scope.ServiceProvider.GetService<IPXRelayService>();
foreach (var server in await serverService.GetAsync(s => s.Autostart && s.AutostartMethod == ServerAutostartMethod.OnApplicationStart))
{
try
{
logger.LogDebug("Autostarting server {ServerName} with a delay of {AutostartDelay} seconds", server.Name, server.AutostartDelay);
Task.Run(() =>
{
if (server.Autostart && server.AutostartDelay > 0)
Task.Delay(TimeSpan.FromSeconds(server.AutostartDelay)).Wait();
return serverProcessService.StartServerAsync(server.Id);
});
}
catch (Exception ex)
{
logger.LogError(ex, "An unexpected error occurred while trying to autostart the server {ServerName}", server.Name);
}
}
}
}

View file

@ -0,0 +1,42 @@
using LANCommander.Server.Services;
using LANCommander.Server.Services.Models;
using Serilog;
namespace LANCommander.Server.Startup;
public static class ApplicationSettings
{
public static WebApplicationBuilder AddSettings(this WebApplicationBuilder builder, out Settings settings)
{
// Add services to the container.
Log.Debug("Loading settings");
if (!File.Exists(SettingService.SettingsFile))
{
var workingDirectory = Path.GetDirectoryName(SettingService.SettingsFile);
if (!String.IsNullOrWhiteSpace(workingDirectory))
Directory.CreateDirectory(workingDirectory);
settings = new Settings();
SettingService.SaveSettings(settings);
}
else
settings = SettingService.GetSettings(true);
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.Services.AddSingleton(settings);
return builder;
}
}

View file

@ -1,24 +1,63 @@
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
using System.Web;
using LANCommander.Server.Data.Models;
using LANCommander.Server.Models;
using LANCommander.Server.Services;
using LANCommander.Server.Services.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Serilog;
using AuthenticationService = LANCommander.Server.Services.AuthenticationService;
namespace LANCommander.Server.Extensions;
namespace LANCommander.Server.Startup;
public static class AuthenticationBuilderExtensions
public static class Authentication
{
public static WebApplicationBuilder ConfigureAuthentication(this WebApplicationBuilder builder, Settings settings)
{
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAntiforgery(options =>
{
options.Cookie.SameSite = settings.Authentication.MinimumSameSitePolicy;
options.Cookie.SecurePolicy = settings.Authentication.CookieSecurePolicy;
});
return builder;
}
public static AuthenticationBuilder AddAuthenticationProviders(this AuthenticationBuilder authBuilder, Settings settings)
{
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);
}
}
return authBuilder;
}
public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder authBuilder, AuthenticationProvider authenticationProvider)
{
if (String.IsNullOrWhiteSpace(authenticationProvider.Slug))

View file

@ -0,0 +1,19 @@
namespace LANCommander.Server.Startup;
public static class Cors
{
public static WebApplicationBuilder AddCors(this WebApplicationBuilder builder)
{
builder.Services.AddCors(static options =>
options.AddPolicy("CorsPolicy", static builder =>
{
builder.AllowAnyHeader()
.AllowAnyMethod()
.SetIsOriginAllowed(static (host) => true)
.AllowCredentials();
})
);
return builder;
}
}

View file

@ -0,0 +1,16 @@
namespace LANCommander.Server.Startup;
public static class Daemon
{
public static WebApplicationBuilder AddAsService(this WebApplicationBuilder builder)
{
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "LANCommander Server";
});
builder.Services.AddSystemd();
return builder;
}
}

View file

@ -0,0 +1,107 @@
using LANCommander.SDK.Enums;
using LANCommander.Server.Data;
using LANCommander.Server.Data.Enums;
using LANCommander.Server.Data.Models;
using LANCommander.Server.Services;
using LANCommander.Server.Services.Models;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LANCommander.Server.Startup;
public static class Database
{
public static WebApplicationBuilder AddDatabase(this WebApplicationBuilder builder)
{
builder.Services.AddDbContextFactory<DatabaseContext>();
builder.Services.AddDbContext<DatabaseContext>();
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
return builder;
}
public static WebApplication UseDatabase(this WebApplication app, string[] args)
{
var settings = app.Services.GetService<Settings>();
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<DatabaseProvider>(databaseProviderParameter);
else
DatabaseContext.Provider = settings.DatabaseProvider;
if (!String.IsNullOrWhiteSpace(connectionStringParameter))
DatabaseContext.ConnectionString = connectionStringParameter;
else
DatabaseContext.ConnectionString = settings.DatabaseConnectionString;
return app;
}
public static async Task MigrateDatabaseAsync(this WebApplication app)
{
if (DatabaseContext.Provider != DatabaseProvider.Unknown)
{
using var scope = app.Services.CreateAsyncScope();
using var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
var settings = scope.ServiceProvider.GetRequiredService<Settings>();
logger.LogDebug("Migrating database if required");
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();
var storageLocationService = scope.ServiceProvider.GetRequiredService<StorageLocationService>();
var archiveLocation = await storageLocationService.AddMissingAsync(l => l.Type == StorageLocationType.Archive && l.Default, new StorageLocation
{
Path = "Uploads",
Type = StorageLocationType.Archive,
Default = true,
});
if (!Directory.Exists(archiveLocation.Value.Path))
Directory.CreateDirectory(archiveLocation.Value.Path);
var mediaLocation = await storageLocationService.AddMissingAsync(l => l.Type == StorageLocationType.Media && l.Default, new StorageLocation
{
Path = "Media",
Type = StorageLocationType.Media,
Default = true,
});
if (!Directory.Exists(mediaLocation.Value.Path))
Directory.CreateDirectory(mediaLocation.Value.Path);
var saveLocation = await storageLocationService.AddMissingAsync(l => l.Type == StorageLocationType.Save && l.Default, new StorageLocation
{
Path = "Saves",
Type = StorageLocationType.Save,
Default = true,
});
if (!Directory.Exists(saveLocation.Value.Path))
Directory.CreateDirectory(saveLocation.Value.Path);
}
else
logger.LogDebug("No pending migrations are available. Skipping database migration.");
}
}
}

View file

@ -0,0 +1,22 @@
using System.Diagnostics;
namespace LANCommander.Server.Startup;
public static class Debug
{
public static WebApplicationBuilder WaitForDebugger(this WebApplicationBuilder builder)
{
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.");
return builder;
}
}

View file

@ -0,0 +1,34 @@
using LANCommander.Server.Endpoints;
using Serilog;
namespace LANCommander.Server.Startup;
public static class Endpoints
{
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;
});
}
public static WebApplication MapEndpoints(this WebApplication app)
{
Log.Debug("Registering Endpoints");
app.UseMvcWithDefaultRoute();
app.UseEndpoints(endpoints =>
{
endpoints.MapDownloadEndpoints();
endpoints.MapSaveEndpoints();
endpoints.MapControllers();
endpoints.MapFallbackToPage("/_Host");
});
return app;
}
}

View file

@ -0,0 +1,30 @@
using LANCommander.Server.Services.Models;
namespace LANCommander.Server.Startup;
public static class Filesystem
{
public static WebApplication PrepareDirectories(this WebApplication app)
{
var settings = app.Services.GetRequiredService<Settings>();
var logger = app.Services.GetRequiredService<ILogger<Program>>();
logger.LogDebug("Ensuring required directories exist");
IEnumerable<string> directories = [
settings.Update.StoragePath,
settings.Launcher.StoragePath,
settings.Backups.StoragePath,
"Snippets",
];
foreach (var directory in directories)
{
logger.LogDebug("Ensuring directory {Directory} exists", directory);
if (!Directory.Exists(directory))
Directory.CreateDirectory(directory);
}
return app;
}
}

View file

@ -0,0 +1,31 @@
using Hangfire;
namespace LANCommander.Server.Startup;
public static class Hangfire
{
public static WebApplicationBuilder AddHangfire(this WebApplicationBuilder builder)
{
builder.Services.AddHangfire(static (sp, configuration) =>
{
var logger = sp.GetRequiredService<ILogger<Program>>();
logger.LogDebug("Initializing Hangfire");
configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseInMemoryStorage();
});
builder.Services.AddHangfireServer();
return builder;
}
public static WebApplication UseHangfire(this WebApplication app)
{
app.UseHangfireDashboard();
return app;
}
}

View file

@ -1,104 +1,72 @@
using LANCommander.Server.Data.Models;
using LANCommander.Server.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using Serilog;
using System.Text;
using LANCommander.Server.Extensions;
using LANCommander.Server.Services.Models;
using Microsoft.AspNetCore.Authentication;
namespace LANCommander.Server;
public static class IdentityExtensions
{
public static void AddIdentity(this WebApplicationBuilder builder, Settings settings)
{
Log.Debug("Initializing Identity");
builder.Services.AddIdentityCore<User>((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<Role>()
.AddEntityFrameworkStores<Data.DatabaseContext>()
.AddUserManager<UserManager<User>>()
.AddSignInManager<SignInManager<User>>()
.AddRoleManager<RoleManager<Role>>()
.AddSignInManager()
.AddDefaultTokenProviders();
builder.Services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.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))
};
})
.AddAuthenticationProviders(settings)
.AddIdentityCookies();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.Secure = settings.Authentication.CookieSecurePolicy;
options.MinimumSameSitePolicy = settings.Authentication.MinimumSameSitePolicy;
});
builder.Services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/Login";
options.LogoutPath = "/Logout";
options.AccessDeniedPath = "/AccessDenied";
});
}
public static AuthenticationBuilder AddAuthenticationProviders(this AuthenticationBuilder authBuilder, Settings settings)
{
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);
}
}
return authBuilder;
}
}
using System.Text;
using LANCommander.Server.Data.Models;
using LANCommander.Server.Services.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using Serilog;
namespace LANCommander.Server.Startup;
public static class Identity
{
public static WebApplicationBuilder AddIdentity(this WebApplicationBuilder builder, Settings settings)
{
Log.Debug("Initializing Identity");
builder.Services.AddIdentityCore<User>((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<Role>()
.AddEntityFrameworkStores<Data.DatabaseContext>()
.AddUserManager<UserManager<User>>()
.AddSignInManager<SignInManager<User>>()
.AddRoleManager<RoleManager<Role>>()
.AddSignInManager()
.AddDefaultTokenProviders();
builder.Services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.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))
};
})
.AddAuthenticationProviders(settings)
.AddIdentityCookies();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.Secure = settings.Authentication.CookieSecurePolicy;
options.MinimumSameSitePolicy = settings.Authentication.MinimumSameSitePolicy;
});
builder.Services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/Login";
options.LogoutPath = "/Logout";
options.AccessDeniedPath = "/AccessDenied";
});
return builder;
}
}

View file

@ -0,0 +1,38 @@
using System.Net;
using LANCommander.Server.Services.Models;
using Microsoft.AspNetCore.Http.Features;
namespace LANCommander.Server.Startup;
public static class Kestrel
{
public static WebApplicationBuilder ConfigureKestrel(this WebApplicationBuilder builder)
{
builder.WebHost.ConfigureKestrel(options =>
{
var settings = options.ApplicationServices.GetRequiredService<Settings>();
options.Limits.MaxRequestBodySize = long.MaxValue;
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(5);
options.Listen(IPAddress.Any, settings.Port);
if (settings.UseSSL)
{
options.Listen(IPAddress.Any, settings.SSLPort, listenOptions =>
{
listenOptions.UseHttps(settings.CertificatePath, settings.CertificatePassword);
});
}
});
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = long.MaxValue;
});
builder.WebHost.UseStaticWebAssets();
return builder;
}
}

View file

@ -1,72 +1,73 @@
using Elastic.Serilog.Sinks;
using LANCommander.Server.Hubs;
using Serilog.Sinks.AspNetCore.App.SignalR.Extensions;
using Serilog;
using LANCommander.Server.Models;
using LANCommander.Server.Services.Models;
using Serilog.Events;
using Serilog.Filters;
namespace LANCommander.Server;
public static class LoggerExtensions
{
public static void AddLogger(this WebApplicationBuilder builder)
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
builder.Services.AddSerilogHub<LoggingHub>();
builder.Services.AddSerilog((serviceProvider, config) =>
{
var settings = serviceProvider.GetRequiredService<Settings>();
if (settings.Logs.IgnorePings)
config.Filter.ByExcluding(Matching.WithProperty<string>("RequestPath", v => v.StartsWith("/api/Ping", StringComparison.OrdinalIgnoreCase)));
foreach (var provider in settings.Logs.Providers)
{
LogEventLevel minimumLevel = provider.MinimumLevel switch
{
LogLevel.Trace => LogEventLevel.Debug,
LogLevel.Debug => LogEventLevel.Debug,
LogLevel.Warning => LogEventLevel.Warning,
LogLevel.Information => LogEventLevel.Information,
LogLevel.Error => LogEventLevel.Error,
LogLevel.Critical => LogEventLevel.Fatal,
_ => LogEventLevel.Information
};
switch (provider.Type)
{
case LoggingProviderType.Console:
config.WriteTo.Console(restrictedToMinimumLevel: minimumLevel);
break;
case LoggingProviderType.SignalR:
config.WriteTo.SignalR<LoggingHub>(
serviceProvider,
(context, message, logEvent) => LoggingHub.Log(context, message, logEvent));
break;
case LoggingProviderType.File:
config.WriteTo.File(
Path.Combine(provider.ConnectionString,"log-.txt"),
rollingInterval: (RollingInterval)(int)(provider.ArchiveEvery ?? LogInterval.Day),
restrictedToMinimumLevel: minimumLevel);
break;
case LoggingProviderType.Seq:
config.WriteTo.Seq(provider.ConnectionString, restrictedToMinimumLevel: minimumLevel);
break;
case LoggingProviderType.ElasticSearch:
config.WriteTo.Elasticsearch([new Uri(provider.ConnectionString)], restrictedToMinimumLevel: minimumLevel);
break;
}
}
});
}
}
using Elastic.Serilog.Sinks;
using LANCommander.Server.Hubs;
using LANCommander.Server.Services.Models;
using Serilog;
using Serilog.Events;
using Serilog.Filters;
using Serilog.Sinks.AspNetCore.App.SignalR.Extensions;
namespace LANCommander.Server.Startup;
public static class Logger
{
public static WebApplicationBuilder AddLogger(this WebApplicationBuilder builder)
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
builder.Services.AddSerilogHub<LoggingHub>();
builder.Services.AddSerilog((serviceProvider, config) =>
{
var settings = serviceProvider.GetRequiredService<Settings>();
if (settings.Logs.IgnorePings)
config.Filter.ByExcluding(Matching.WithProperty<string>("RequestPath", v => v.StartsWith("/api/Ping", StringComparison.OrdinalIgnoreCase)));
foreach (var provider in settings.Logs.Providers)
{
LogEventLevel minimumLevel = provider.MinimumLevel switch
{
LogLevel.Trace => LogEventLevel.Debug,
LogLevel.Debug => LogEventLevel.Debug,
LogLevel.Warning => LogEventLevel.Warning,
LogLevel.Information => LogEventLevel.Information,
LogLevel.Error => LogEventLevel.Error,
LogLevel.Critical => LogEventLevel.Fatal,
_ => LogEventLevel.Information
};
switch (provider.Type)
{
case LoggingProviderType.Console:
config.WriteTo.Console(restrictedToMinimumLevel: minimumLevel);
break;
case LoggingProviderType.SignalR:
config.WriteTo.SignalR<LoggingHub>(
serviceProvider,
(context, message, logEvent) => LoggingHub.Log(context, message, logEvent));
break;
case LoggingProviderType.File:
config.WriteTo.File(
Path.Combine(provider.ConnectionString,"log-.txt"),
rollingInterval: (RollingInterval)(int)(provider.ArchiveEvery ?? LogInterval.Day),
restrictedToMinimumLevel: minimumLevel);
break;
case LoggingProviderType.Seq:
config.WriteTo.Seq(provider.ConnectionString, restrictedToMinimumLevel: minimumLevel);
break;
case LoggingProviderType.ElasticSearch:
config.WriteTo.Elasticsearch([new Uri(provider.ConnectionString)], restrictedToMinimumLevel: minimumLevel);
break;
}
}
});
return builder;
}
}

View file

@ -0,0 +1,20 @@
using Microsoft.AspNetCore.HttpOverrides;
namespace LANCommander.Server.Startup;
public static class Middleware
{
public static WebApplication UseMiddlewares(this WebApplication app)
{
app.UseMiddleware<RobotsMiddleware>();
app.UseMiddleware<ApiVersioningMiddleware>();
app.UseMiddleware<PingMiddleware>();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
return app;
}
}

View file

@ -0,0 +1,47 @@
using Serilog;
namespace LANCommander.Server.Startup;
public static class Razor
{
public static WebApplicationBuilder 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
.AddRazorComponents()
.AddInteractiveServerComponents();
return builder;
}
}

View file

@ -0,0 +1,46 @@
using LANCommander.SDK.Enums;
using LANCommander.Server.Data;
using LANCommander.Server.Data.Enums;
using LANCommander.Server.Services;
namespace LANCommander.Server.Startup;
public static class Servers
{
public static async Task StartServerProcessesAsync(this WebApplication app)
{
if (DatabaseContext.Provider != DatabaseProvider.Unknown)
{
// Autostart any server processes
using var scope = app.Services.CreateScope();
var serverService = scope.ServiceProvider.GetRequiredService<ServerService>();
var serverProcessService = scope.ServiceProvider.GetRequiredService<ServerProcessService>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogDebug("Autostarting Servers");
// Autostart IPX relay
scope.ServiceProvider.GetService<IPXRelayService>();
foreach (var server in await serverService.GetAsync(s => s.Autostart && s.AutostartMethod == ServerAutostartMethod.OnApplicationStart))
{
try
{
logger.LogDebug("Autostarting server {ServerName} with a delay of {AutostartDelay} seconds", server.Name, server.AutostartDelay);
Task.Run(() =>
{
if (server.Autostart && server.AutostartDelay > 0)
Task.Delay(TimeSpan.FromSeconds(server.AutostartDelay)).Wait();
return serverProcessService.StartServerAsync(server.Id);
});
}
catch (Exception ex)
{
logger.LogError(ex, "An unexpected error occurred while trying to autostart the server {ServerName}", server.Name);
}
}
}
}
}

View file

@ -1,104 +1,72 @@
using LANCommander.SDK;
using LANCommander.Server.Services.MediaGrabbers;
using LANCommander.Server.Services;
using Serilog;
using Hangfire;
using LANCommander.Server.Data;
using LANCommander.Server.Services.Factories;
using LANCommander.Server.Services.Importers;
using LANCommander.Server.Services.Models;
namespace LANCommander.Server;
public static class ServiceExtensions
{
public static void AddLANCommanderServices(this WebApplicationBuilder builder, Settings settings)
{
Log.Debug("Registering services");
builder.Services.AddSingleton(new Client("", ""));
builder.Services.AddScoped<IdentityContextFactory>();
builder.Services.AddScoped<SettingService>();
builder.Services.AddScoped<ArchiveService>();
builder.Services.AddScoped<StorageLocationService>();
builder.Services.AddScoped<CategoryService>();
builder.Services.AddScoped<CollectionService>();
builder.Services.AddScoped<GameService>();
builder.Services.AddScoped<LibraryService>();
builder.Services.AddScoped<ScriptService>();
builder.Services.AddScoped<GenreService>();
builder.Services.AddScoped<PlatformService>();
builder.Services.AddScoped<KeyService>();
builder.Services.AddScoped<TagService>();
builder.Services.AddScoped<EngineService>();
builder.Services.AddScoped<CompanyService>();
builder.Services.AddScoped<IGDBService>();
builder.Services.AddScoped<ServerService>();
builder.Services.AddScoped<ServerConsoleService>();
builder.Services.AddScoped<GameSaveService>();
builder.Services.AddScoped<PlaySessionService>();
builder.Services.AddScoped<MediaService>();
builder.Services.AddScoped<RedistributableService>();
builder.Services.AddScoped<IMediaGrabberService, SteamGridDBMediaGrabber>();
builder.Services.AddScoped<UpdateService>();
builder.Services.AddScoped<IssueService>();
builder.Services.AddScoped<PageService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<RoleService>();
builder.Services.AddScoped<UserCustomFieldService>();
builder.Services.AddScoped<AuthenticationService>();
builder.Services.AddTransient<SetupService>();
builder.Services.AddScoped(typeof(ImportService<>));
// Register importers
builder.Services.AddScoped<IImporter<Data.Models.Game>, GameImporter>();
builder.Services.AddScoped<IImporter<Data.Models.Server>, ServerImporter>();
builder.Services.AddScoped<IImporter<Data.Models.Redistributable>, RedistributableImporter>();
builder.Services.AddSingleton<ServerProcessService>();
builder.Services.AddSingleton<IPXRelayService>();
builder.Services.AddAutoMapper(typeof(LANCommanderMappingProfile));
builder.Services.AddFusionCache();
builder.Services.AddAntDesign();
builder.Services.AddHttpClient();
builder.Services.AddHttpContextAccessor();
if (settings.Beacon.Enabled)
{
Log.Debug("The beacons have been lit! LANCommander calls for players!");
builder.Services.AddHostedService<BeaconService>();
}
}
public static void AddAsService(this WebApplicationBuilder builder)
{
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "LANCommander Server";
});
builder.Services.AddSystemd();
}
public static void AddHangfire(this WebApplicationBuilder builder)
{
builder.Services.AddHangfire(static (sp, configuration) =>
{
var logger = sp.GetRequiredService<ILogger<Program>>();
logger.LogDebug("Initializing Hangfire");
configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseInMemoryStorage();
});
builder.Services.AddHangfireServer();
}
public static void AddDatabase(this WebApplicationBuilder builder)
{
builder.Services.AddDbContextFactory<DatabaseContext>();
builder.Services.AddDbContext<DatabaseContext>();
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
}
}
using LANCommander.SDK;
using LANCommander.Server.Services;
using LANCommander.Server.Services.Factories;
using LANCommander.Server.Services.Importers;
using LANCommander.Server.Services.MediaGrabbers;
using LANCommander.Server.Services.Models;
using Serilog;
namespace LANCommander.Server.Startup;
public static class Services
{
public static WebApplicationBuilder AddLANCommanderServices(this WebApplicationBuilder builder, Settings settings)
{
Log.Debug("Registering services");
builder.Services.AddSingleton(new Client("", ""));
builder.Services.AddScoped<IdentityContextFactory>();
builder.Services.AddScoped<SettingService>();
builder.Services.AddScoped<ArchiveService>();
builder.Services.AddScoped<StorageLocationService>();
builder.Services.AddScoped<CategoryService>();
builder.Services.AddScoped<CollectionService>();
builder.Services.AddScoped<GameService>();
builder.Services.AddScoped<LibraryService>();
builder.Services.AddScoped<ScriptService>();
builder.Services.AddScoped<GenreService>();
builder.Services.AddScoped<PlatformService>();
builder.Services.AddScoped<KeyService>();
builder.Services.AddScoped<TagService>();
builder.Services.AddScoped<EngineService>();
builder.Services.AddScoped<CompanyService>();
builder.Services.AddScoped<IGDBService>();
builder.Services.AddScoped<ServerService>();
builder.Services.AddScoped<ServerConsoleService>();
builder.Services.AddScoped<GameSaveService>();
builder.Services.AddScoped<PlaySessionService>();
builder.Services.AddScoped<MediaService>();
builder.Services.AddScoped<RedistributableService>();
builder.Services.AddScoped<IMediaGrabberService, SteamGridDBMediaGrabber>();
builder.Services.AddScoped<UpdateService>();
builder.Services.AddScoped<IssueService>();
builder.Services.AddScoped<PageService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<RoleService>();
builder.Services.AddScoped<UserCustomFieldService>();
builder.Services.AddScoped<AuthenticationService>();
builder.Services.AddTransient<SetupService>();
builder.Services.AddScoped(typeof(ImportService<>));
// Register importers
builder.Services.AddScoped<IImporter<Data.Models.Game>, GameImporter>();
builder.Services.AddScoped<IImporter<Data.Models.Server>, ServerImporter>();
builder.Services.AddScoped<IImporter<Data.Models.Redistributable>, RedistributableImporter>();
builder.Services.AddSingleton<ServerProcessService>();
builder.Services.AddSingleton<IPXRelayService>();
builder.Services.AddAutoMapper(typeof(LANCommanderMappingProfile));
builder.Services.AddFusionCache();
builder.Services.AddAntDesign();
builder.Services.AddHttpClient();
builder.Services.AddHttpContextAccessor();
if (settings.Beacon.Enabled)
{
Log.Debug("The beacons have been lit! LANCommander calls for players!");
builder.Services.AddHostedService<BeaconService>();
}
return builder;
}
}

View file

@ -0,0 +1,22 @@
using LANCommander.Server.Hubs;
namespace LANCommander.Server.Startup;
public static class SignalR
{
public static void AddSignalR(this WebApplicationBuilder builder)
{
builder.Services.AddSignalR().AddJsonProtocol(static options =>
{
options.PayloadSerializerOptions.PropertyNamingPolicy = null;
});
}
public static WebApplication UseSignalR(this WebApplication app)
{
app.MapHub<GameServerHub>("/hubs/gameserver");
app.MapHub<LoggingHub>("/logging");
return app;
}
}

View file

@ -0,0 +1,14 @@
using Hangfire;
using LANCommander.Server.Jobs.Background;
namespace LANCommander.Server.Startup;
public static class Thumbnails
{
public static WebApplication GenerateThumbnails(this WebApplication app)
{
BackgroundJob.Enqueue<GenerateThumbnailsJob>(x => x.ExecuteAsync());
return app;
}
}