using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.Authentication; using System.Text; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Server.Kestrel.Https; using SPTarkov.Common.Semver; using SPTarkov.Common.Semver.Implementations; using SPTarkov.DI; using SPTarkov.Reflection.Patching; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Loaders; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Spt.Mod; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Utils; using SPTarkov.Server.Core.Utils.Logger; using SPTarkov.Server.Logger; using SPTarkov.Server.Modding; using SPTarkov.Server.Services; using SPTarkov.Server.Web; namespace SPTarkov.Server; public static class Program { public static async Task Main(string[] args) { try { await StartServer(args); } catch (SocketException) { Console.WriteLine("========================================================================================================="); Console.WriteLine("You have multiple servers running or another process using port 6969"); Console.WriteLine("========================================================================================================="); Console.WriteLine("Press any key to exit..."); Console.ReadLine(); } catch (Exception e) { if ( e.Message.Contains( "could not load file or assembly 'sptarkov.server.core, version=", StringComparison.InvariantCultureIgnoreCase ) ) { ShowRedConsoleMessage( e, "You may have installed a mod that needs a newer version of of SPT installed. Please try updating SPT" ); Console.ReadLine(); return; } if (e.Message.Contains("could not load file or assembly", StringComparison.InvariantCultureIgnoreCase)) { ShowRedConsoleMessage( e, "You may have forgotten to install a requirement for one of your mods, please check the mod page again and install any dependencies listed. Read the below error message CAREFULLY to find the name of the mod you need to install" ); Console.ReadLine(); return; } ShowRedConsoleMessage( e, "The server has unexpectedly stopped, reach out to #mod-questions-4-0 in our Discord server. Include a screenshot of this message and the surrounding error(s) above and below" ); Console.WriteLine("Press any key to exit..."); Console.ReadLine(); } } private static void ShowRedConsoleMessage(Exception e, string message) { Console.WriteLine("========================================================================================================="); Console.BackgroundColor = ConsoleColor.DarkRed; Console.ForegroundColor = ConsoleColor.Black; Console.WriteLine(message); Console.ResetColor(); Console.WriteLine(e); Console.WriteLine("========================================================================================================="); } public static async Task StartServer(string[] args) { Console.OutputEncoding = Encoding.UTF8; SetConsoleOutputMode(); // Some users don't know how to create a shortcut... if (!IsRunFromInstallationFolder()) { Console.WriteLine("You have not created a shortcut properly. Please hold alt when dragging to create a shortcut."); await Task.Delay(-1); return; } // Initialize the program variables ProgramStatics.Initialize(); // Create web builder and logger var builder = CreateNewHostBuilder(); var diHandler = new DependencyInjectionHandler(builder.Services); // register SPT components diHandler.AddInjectableTypesFromTypeAssembly(typeof(Program)); diHandler.AddInjectableTypesFromTypeAssembly(typeof(App)); diHandler.AddInjectableTypesFromTypeAssembly(typeof(PatchManager)); List loadedMods = []; if (ProgramStatics.MODS()) { // Search for mod dlls loadedMods = ModDllLoader.LoadAllMods(); // validate and sort mods, this will also discard any mods that are invalid var validatedLoadedMods = ValidateMods(loadedMods); // update the loadedMods list with our validated mods loadedMods = validatedLoadedMods; diHandler.AddInjectableTypesFromAssemblies(validatedLoadedMods.SelectMany(a => a.Assemblies)); } diHandler.InjectAll(); builder.InitializeSptBlazor(loadedMods); builder.Services.AddSingleton(builder); builder.Services.AddSingleton>(loadedMods); // Configure Kestrel options ConfigureKestrel(builder); var app = builder.Build(); // Configure Kestrel WS options and Handle fallback requests ConfigureWebApp(app); // In case of exceptions we snatch a Server logger var serverExceptionLogger = app.Services.GetRequiredService().CreateLogger("Server"); // We need any logger instance to use as a finalizer when the app closes var loggerFinalizer = app.Services.GetRequiredService>(); try { // Handle edge cases where reverse proxies might pass X-Forwarded-For, use this as the actual IP address var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, ForwardLimit = null, }; forwardedHeadersOptions.KnownNetworks.Clear(); forwardedHeadersOptions.KnownProxies.Clear(); app.UseForwardedHeaders(forwardedHeadersOptions); await app.Services.GetRequiredService().Startup(); await app.RunAsync(); } catch (Exception ex) { serverExceptionLogger.LogCritical(ex, "Critical exception, stopping server..."); throw; } finally { loggerFinalizer.DumpAndStop(); } } private static void ConfigureWebApp(WebApplication app) { app.UseWebSockets( new WebSocketOptions { // Every minute a heartbeat is sent to keep the connection alive. KeepAliveInterval = TimeSpan.FromSeconds(60), } ); app.UseMiddleware(); app.UseNoGCRegions(); app.Use(async (context, next) => await context.RequestServices.GetRequiredService().HandleRequest(context, next)); app.UseSptBlazor(); } private static void ConfigureKestrel(WebApplicationBuilder builder) { builder.WebHost.ConfigureKestrel( (_, options) => { // This method is not expected to be async so we need to wait for the Task instead of using await keyword options.ApplicationServices.GetRequiredService().OnLoad().Wait(); var httpConfig = options.ApplicationServices.GetRequiredService().GetConfig(); // Probe the http ip and port to see if its being used, this method will throw an exception and crash // the server if the IP/Port combination is already in use TcpListener? listener = null; try { listener = new TcpListener(IPAddress.Parse(httpConfig.Ip), httpConfig.Port); listener.Start(); } finally { listener?.Stop(); } var certHelper = options.ApplicationServices.GetRequiredService(); options.Listen( IPAddress.Parse(httpConfig.Ip), httpConfig.Port, listenOptions => { listenOptions.UseHttps(opts => { opts.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13; opts.ServerCertificate = certHelper.LoadOrGenerateCertificatePfx(); opts.ClientCertificateMode = ClientCertificateMode.NoCertificate; }); } ); } ); } private static WebApplicationBuilder CreateNewHostBuilder() { var builder = WebApplication.CreateBuilder(new WebApplicationOptions { WebRootPath = "./SPT_Data/wwwroot" }); builder.Logging.ClearProviders(); builder.Configuration.SetBasePath(Directory.GetCurrentDirectory()); builder.Host.UseSptLogger(); return builder; } private static List ValidateMods(IEnumerable mods) { if (!ProgramStatics.MODS()) { return []; } // We need the SPT dependencies for the ModValidator, but mods are loaded before the web application // So we create a disposable web application that we will throw away after getting the mods to load var builder = CreateNewHostBuilder(); // register SPT components var diHandler = new DependencyInjectionHandler(builder.Services); diHandler.AddInjectableTypesFromAssembly(typeof(Program).Assembly); diHandler.AddInjectableTypesFromAssembly(typeof(App).Assembly); diHandler.InjectAll(); // register the mod validator components var provider = builder .Services.AddScoped(typeof(ISptLogger), typeof(SptLogger)) .AddScoped(typeof(ISemVer), typeof(SemanticVersioningSemVer)) .AddSingleton() .BuildServiceProvider(); var modValidator = provider.GetRequiredService(); return modValidator.ValidateMods(mods); } private static void SetConsoleOutputMode() { var disableFlag = Environment.GetEnvironmentVariable("DISABLE_VIRTUAL_TERMINAL"); if (!OperatingSystem.IsWindows() || disableFlag == "1" || string.Equals(disableFlag, "true", StringComparison.OrdinalIgnoreCase)) { return; } const int stdOutputHandle = -11; const uint enableVirtualTerminalProcessing = 0x0004; var handle = GetStdHandle(stdOutputHandle); if (!GetConsoleMode(handle, out var consoleMode)) { throw new Exception("Unable to get console mode"); } consoleMode |= enableVirtualTerminalProcessing; if (!SetConsoleMode(handle, consoleMode)) { throw new Exception("Unable to set console mode"); } } private static bool IsRunFromInstallationFolder() { var dirFiles = Directory.GetFiles(Directory.GetCurrentDirectory()); // This file is guaranteed to exist if ran from the correct location, even if the game does not exist here. return dirFiles.Any(dirFile => dirFile.EndsWith("sptLogger.json") || dirFile.EndsWith("sptLogger.Development.json")); } [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr GetStdHandle(int nStdHandle); [DllImport("kernel32.dll")] private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); [DllImport("kernel32.dll")] private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); }