internal async Task Init() { var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); XmlConfigurator.Configure(logRepository, new FileInfo("App.config")); //"log4net.config" if (!new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) { WriteAndWaitForKey($@"This application must be run as administrator in order to function properly."); return; } //ArkToolkitDomain.Initialize(); //File.WriteAllText(@"ark.json", JsonConvert.SerializeObject(ArkSavegameToolkitNet.ArkToolkitSettings.Instance, Formatting.Indented)); var arkConfigCustom = false; if (File.Exists(Constants.ArkConfigFilePath)) { try { // use custom settings from ark.json ArkToolkitSettings.Instance.Setup(JsonConvert.DeserializeObject <ArkToolkitSettings>(File.ReadAllText(Constants.ArkConfigFilePath))); arkConfigCustom = true; } catch (Exception ex) { Console.AddLogError($@"Error loading 'ark.config'. Using default config. (""{ex.Message}"")"); Logging.LogException("Error loading 'ark.config'. Using default config.", ex, GetType()); } } if (!arkConfigCustom) { // initialize default settings ArkToolkitDomain.Initialize(); } _config = null; string validationMessage = null; string errorMessage = null; if (File.Exists(Constants.ConfigFilePath)) { try { _config = JsonConvert.DeserializeObject <Config>(File.ReadAllText(Constants.ConfigFilePath)); if (_config != null) { if (_config.Discord == null) { _config.Discord = new DiscordConfigSection(); } _config.SetupDefaults(); } else { errorMessage = "Config.json is empty. Please delete it and restart the application."; } } catch (Exception ex) { validationMessage = ex.Message; } } var hasValidConfig = _config != null; if (_config == null) { //load defaultconfig if (!File.Exists(Constants.DefaultConfigFilePath)) { WriteAndWaitForKey($@"The required file defaultconfig.json is missing from application directory. Please redownload the application."); return; } try { _config = JsonConvert.DeserializeObject <Config>(File.ReadAllText(Constants.DefaultConfigFilePath)); } catch (Exception ex) { } WriteAndWaitForKey( $@"The file config.json is empty or contains errors. Skipping automatic startup...", validationMessage); } Configuration.Config = _config as Config; About.HasValidConfig = hasValidConfig; About.ValidationError = validationMessage; About.ConfigError = errorMessage; if (!hasValidConfig) { About.IsActive = true; return; } else { About.IsVisible = false; Console.IsActive = true; } string errors = ValidateConfig(); if (errors.Length > 0) { WriteAndWaitForKey(errors); return; } IProgress <string> progress = new Progress <string>(message => { Console.AddLog(message); }); var constants = new Shared.Constants(); //if (config.Debug) //{ // //we reset the state so that every run will be the same // if (File.Exists(constants.DatabaseFilePath)) File.Delete(constants.DatabaseFilePath); // if (File.Exists(constants.SavedStateFilePath)) File.Delete(constants.SavedStateFilePath); // //optionally use a saved database state // var databaseStateFilePath = Path.Combine(config.JsonOutputDirPath, "Database.state"); // if (File.Exists(databaseStateFilePath)) File.Copy(databaseStateFilePath, constants.DatabaseFilePath); //} _savedstate = null; try { if (File.Exists(constants.SavedStateFilePath)) { _savedstate = JsonConvert.DeserializeObject <SavedState>(File.ReadAllText(constants.SavedStateFilePath)); _savedstate._Path = constants.SavedStateFilePath; } } catch { /*ignore exceptions */ } _savedstate = _savedstate ?? new SavedState(constants.SavedStateFilePath); var discord = new DiscordSocketClient(new DiscordSocketConfig { LogLevel = _config.Discord.LogLevel }); discord.Log += msg => { Console.AddLog(msg.Message); return(Task.CompletedTask); }; var discordCommands = new CommandService(new CommandServiceConfig { }); var anonymizeData = new ArkBotAnonymizeData(); //setup dependency injection var thisAssembly = Assembly.GetExecutingAssembly(); var builder = new ContainerBuilder(); builder.RegisterType <ArkServerContext>().AsSelf(); builder.RegisterInstance(anonymizeData).AsSelf().As <ArkAnonymizeData>(); if (_config.UseCompatibilityChangeWatcher) { builder.RegisterType <ArkSaveFileWatcherTimer>().As <IArkSaveFileWatcher>(); } else { builder.RegisterType <ArkSaveFileWatcher>().As <IArkSaveFileWatcher>(); } builder.RegisterInstance(discord).AsSelf(); builder.RegisterInstance(discordCommands).AsSelf(); builder.RegisterType <AutofacDiscordServiceProvider>().As <IServiceProvider>().SingleInstance(); builder.RegisterType <ArkDiscordBot>(); builder.RegisterInstance(constants).As <IConstants>(); builder.RegisterInstance(_savedstate).As <ISavedState>(); builder.RegisterInstance(_config as Config).As <IConfig>(); builder.RegisterType <EfDatabaseContext>().AsSelf().As <IEfDatabaseContext>() .WithParameter(new TypedParameter(typeof(string), _config.DatabaseConnectionString)); builder.RegisterType <EfDatabaseContextFactory>(); builder.RegisterType <DatabaseRepo>().As <IDatabaseRepo>().SingleInstance(); builder.RegisterType <ArkServerService>().As <IArkServerService>().SingleInstance(); builder.RegisterType <SavegameBackupService>().As <ISavegameBackupService>().SingleInstance(); builder.RegisterType <PlayerLastActiveService>().As <IPlayerLastActiveService>().SingleInstance(); builder.RegisterType <LogCleanupService>().As <ILogCleanupService>().SingleInstance(); builder.RegisterHubs(Assembly.GetExecutingAssembly()); builder.RegisterType <ArkContextManager>().WithParameter(new TypedParameter(typeof(IProgress <string>), progress)).AsSelf().SingleInstance(); builder.RegisterType <DiscordManager>().AsSelf().SingleInstance(); builder.RegisterType <ScheduledTasksManager>().AsSelf().PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies).SingleInstance(); builder.RegisterType <NotificationManager>().AsSelf().SingleInstance(); builder.RegisterType <PrometheusManager>().AsSelf().SingleInstance(); builder.RegisterType <AuctionHouseManager>().AsSelf().SingleInstance(); Container = builder.Build(); //update database try { using (var db = Container.Resolve <EfDatabaseContext>()) { db.Database.Migrate(); } } catch (SqlException ex) { Console.AddLogError($@"Error initializing Microsoft SQL Server (""{ex.Message}"")"); Logging.LogException("Error initializing Microsoft SQL Server.", ex, GetType()); return; } _contextManager = Container.Resolve <ArkContextManager>(); //server/cluster contexts if (_config.Clusters?.Count > 0) { foreach (var cluster in _config.Clusters) { var context = new ArkClusterContext(cluster, anonymizeData); _contextManager.AddCluster(context); } } if (_config.Servers?.Count > 0) { var playerLastActiveService = Container.Resolve <IPlayerLastActiveService>(); var backupService = Container.Resolve <ISavegameBackupService>(); var logCleanupService = Container.Resolve <ILogCleanupService>(); foreach (var server in _config.Servers) { var clusterContext = _contextManager.GetCluster(server.ClusterKey); var context = Container.Resolve <ArkServerContext>( new TypedParameter(typeof(ServerConfigSection), server), new TypedParameter(typeof(ArkClusterContext), clusterContext)); var initTask = context.Initialize(); //fire and forget _contextManager.AddServer(context); } // Initialize managers so that they are ready to handle events such as ArkContextManager.InitializationCompleted-event. var scheduledTasksManager = Container.Resolve <ScheduledTasksManager>(); var notificationManager = Container.Resolve <NotificationManager>(); // <GHOST DIVISION> var prometheusManager = Container.Resolve <PrometheusManager>(); var auctionHouseManager = Container.Resolve <AuctionHouseManager>(); // </GHOST DIVISION> // Trigger manual updates for all servers (initialization) foreach (var context in _contextManager.Servers) { ManuallyUpdateServers.Add(new MenuItemViewModel { Header = context.Config.Key, Command = new DelegateCommand <string>(OnManuallyTriggerServerUpdate), CommandParameter = context.Config.Key }); _contextManager.QueueUpdateServerManual(context); } // Trigger manual updates for all clusters (initialization) foreach (var context in _contextManager.Clusters) { ManuallyUpdateClusters.Add(new MenuItemViewModel { Header = context.Config.Key, Command = new DelegateCommand <string>(OnManuallyTriggerClusterUpdate), CommandParameter = context.Config.Key }); _contextManager.QueueUpdateClusterManual(context); } // <GHOST DIVISION> // run prometheus endpoint if (_config.Prometheus.Enabled) { prometheusManager.Start(); } else { Console.AddLog("Prometheus is disabled."); } // run auction house manager if (_config.AuctionHouse.Enabled) { auctionHouseManager.Start(); } else { Console.AddLog("Auction House monitor is disabled."); } // </GHOST DIVISION> } //run the discord bot if (_config.Discord.Enabled) { _runDiscordBotCts = new CancellationTokenSource(); _runDiscordBotTask = await Task.Factory.StartNew(async() => await RunDiscordBot(), _runDiscordBotCts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } else { Console.AddLog("Discord bot is disabled."); } //load the server multipliers data await ArkServerMultipliers.Instance.LoadOrUpdate(); var modIds = _config.Servers?.SelectMany(x => x.ModIds).Distinct().ToArray() ?? new int[] { }; //load the species stats data await ArkSpeciesStats.Instance.LoadOrUpdate(modIds); //load the items data await ArkItems.Instance.LoadOrUpdate(modIds); //ssl if (_config.WebApp.Ssl?.Enabled == true) { var path = $"{_config.WebApp.Ssl.Name}.pfx"; var revoke = false; var renew = false; if (File.Exists(path)) { try { using (var rlt = new X509Certificate2(path, _config.WebApp.Ssl.Password, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet)) { if (DateTime.Now < rlt.NotBefore || DateTime.Now > rlt.NotAfter.AddDays(-31)) { using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine)) { store.Open(OpenFlags.ReadWrite); if (store.Certificates.Contains(rlt)) { store.Remove(rlt); } store.Close(); } renew = revoke = true; } } } catch (Exception ex) { Logging.LogException("Failed to remove ssl certificate from store.", ex, GetType()); } } else { renew = true; } if (renew) { var success = false; Console.AddLog(@"SSL Certificate request issued..."); AcmeContext acme = null; try { var pathAccountKey = $"{_config.WebApp.Ssl.Name}-account.pem"; if (File.Exists(pathAccountKey)) { var accountKey = KeyFactory.FromPem(File.ReadAllText(pathAccountKey)); acme = new AcmeContext(WellKnownServers.LetsEncryptV2, accountKey); var account = await acme.Account(); } else { acme = new AcmeContext(WellKnownServers.LetsEncryptV2); var account = await acme.NewAccount(_config.WebApp.Ssl.Email, true); var pemKey = acme.AccountKey.ToPem(); File.WriteAllText(pathAccountKey, pemKey); } var order = await acme.NewOrder(_config.WebApp.Ssl.Domains); var authz = (await order.Authorizations()).First(); var httpChallenge = await authz.Http(); var keyAuthz = httpChallenge.KeyAuthz; using (var webapp = Host.CreateDefaultBuilder() .UseServiceProviderFactory(new AutofacChildLifetimeScopeServiceProviderFactory(Container.BeginLifetimeScope("AspNetCore_IsolatedRoot_SSL_Renew"))) .ConfigureWebHostDefaults(webBuilder => { webBuilder .ConfigureLogging(logging => { logging.ClearProviders(); logging.AddDebug(); logging.AddWebApp(); }) .UseUrls(_config.WebApp.Ssl.ChallengeListenPrefix) .Configure((appBuilder) => { appBuilder.UseRouting(); appBuilder.UseEndpoints(endpoints => { endpoints.MapGet($"/.well-known/acme-challenge/{httpChallenge.Token}", context => { context.Response.ContentType = "application/text"; context.Response.StatusCode = StatusCodes.Status200OK; return(context.Response.WriteAsync(keyAuthz)); }); }); }); }).Build()) { (webapp as IHost).Start(); var result = await httpChallenge.Validate(); while (result.Status == Certes.Acme.Resource.ChallengeStatus.Pending || result.Status == Certes.Acme.Resource.ChallengeStatus.Processing) { await Task.Delay(500); result = await httpChallenge.Resource(); } if (result.Status == Certes.Acme.Resource.ChallengeStatus.Valid) { var privateKey = KeyFactory.NewKey(KeyAlgorithm.ES256); var cert = await order.Generate(new CsrInfo { CountryName = new RegionInfo(CultureInfo.CurrentCulture.Name).TwoLetterISORegionName, Locality = "Web", Organization = "ARK Bot", }, privateKey); if (revoke) { //await acme.RevokeCertificate( // File.ReadAllBytes(path), // Certes.Acme.Resource.RevocationReason.Superseded, // KeyFactory.FromPem(File.ReadAllText(pathPrivateKey))); if (File.Exists(path)) { File.Delete(path); } } //File.WriteAllText(pathPrivateKey, privateKey.ToPem()); var pfxBuilder = cert.ToPfx(privateKey); var pfx = pfxBuilder.Build(_config.WebApp.Ssl.Name, _config.WebApp.Ssl.Password); File.WriteAllBytes(path, pfx); success = true; } } if (success) { Console.AddLog(@"SSL Certificate request completed!"); } else { Console.AddLog(@"SSL Certificate challenge failed!"); } } catch (Exception ex) { Console.AddLogError($@"SSL Certificate request failed! (""{ex.Message}"")"); Logging.LogException("Failed to issue ssl certificate.", ex, GetType()); } } // clean up all bindings created by ark bot { var(success, output) = await ProcessHelper.RunCommandLine("netsh http show sslcert", _config); if (success && !string.IsNullOrEmpty(output)) { var r = new Regex($@"(?:(?:\s*IP\:port\s*\:\s*(?<ip>[^\:]+?)\:(?<port>\d+))|(?:\s*Hostname\:port\s*\:\s*(?<hostname>[^\:]+?)\:(?<port>\d+))).*Application ID\s*\:\s*{{{Constants.AppId}}}", RegexOptions.IgnoreCase | RegexOptions.Singleline); var sections = output.Split("\r\n\r\n\r\n", StringSplitOptions.RemoveEmptyEntries); foreach (var section in sections) { var m = r.Match(section); if (!m.Success) { continue; } if (m.Groups["hostname"].Success && m.Groups["port"].Success) { await ProcessHelper.RunCommandLine($"netsh http delete sslcert hostnameport={m.Groups["hostname"].Value}:{m.Groups["port"].Value}", _config); } else if (m.Groups["ip"].Success && m.Groups["port"].Success) { await ProcessHelper.RunCommandLine($"netsh http delete sslcert ipport={m.Groups["ip"].Value}:{m.Groups["port"].Value}", _config); } } } } } // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/web-host?view=aspnetcore-3.1 try { _webapp = Host.CreateDefaultBuilder() .UseServiceProviderFactory(new AutofacChildLifetimeScopeServiceProviderFactory(Container.BeginLifetimeScope("AspNetCore_IsolatedRoot"))) .ConfigureWebHostDefaults(webBuilder => { webBuilder.ConfigureKestrel(options => { options.Listen(IPEndPoint.Parse(_config.WebApp.IPEndpoint), listenOptions => { if (_config.WebApp.Ssl.Enabled) { listenOptions.UseHttps($"../{_config.WebApp.Ssl.Name}.pfx", _config.WebApp.Ssl.Password); } }); }) .ConfigureLogging(logging => { logging.ClearProviders(); logging.AddDebug(); logging.AddWebApp(); }) .UseContentRoot(Path.Combine(AppContext.BaseDirectory, "WebApp")) //Directory.GetCurrentDirectory() .UseWebRoot(Path.Combine(AppContext.BaseDirectory, "WebApp")) .UseStartup <WebAppStartup>(); }).Build(); (_webapp as IHost).Start(); Console.AddLog("Web App started"); } catch (IOException ex) when((ex.HResult & 0x0000FFFF) == 5664) //Failed to bind to address <...>: address already in use. { var portInUseBy = (string)null; try { if (IPEndPoint.TryParse(_config.WebApp.IPEndpoint, out var ipEndpoint)) { // power shell // Get-Process -Id (Get-NetTCPConnection -LocalPort 8600).OwningProcess | Format-List * var(success, output) = await ProcessHelper.RunCommandLine($"(Get-Process -Id (Get-NetTCPConnection -LocalPort {ipEndpoint.Port}).OwningProcess).Name", _config, UseCallOperator : false); if (success && !string.IsNullOrEmpty(output)) { portInUseBy = $@"Port {ipEndpoint.Port} in use by: {output}"; } } } catch (Exception) { /* do nothing */ } Console.AddLogError($@"Failed to start web app! (""{ex.Message}"")"); if (!string.IsNullOrWhiteSpace(portInUseBy)) { Console.AddLogError(portInUseBy); } Logging.LogException("Failed to start web app.", ex, GetType()); } }