// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Net; using EpicGames.Horde.Server; using EpicGames.Horde.Utilities; using HordeServer.Utilities; using Microsoft.Extensions.Logging; namespace HordeServer { /// /// Feature flags to aid rollout of new features. /// /// Once a feature is running in its intended state and is stable, the flag should be removed. /// A name and date of when the flag was created is noted next to it to help encourage this behavior. /// Try having them be just a flag, a boolean. /// public class FeatureFlagSettings { } /// /// Global settings for the application /// [ConfigDoc("Server.json", "[Horde](../../README.md) > [Deployment](../Deployment.md) > [Server](Server.md)", "Deployment/ServerSettings.md", Introduction = "All Horde-specific settings are stored in a root object called `Horde`. Other .NET functionality may be configured using properties in the root of this file." )] public class ServerSettings { /// /// Name of the section containing these settings /// public const string SectionName = "Horde"; /// /// Modes that the server should run in. Runmodes can be used in a multi-server deployment to limit the operations that a particular instance will try to perform. /// public RunMode[]? RunModes { get; set; } = null; /// /// Override the data directory used by Horde. Defaults to C:\ProgramData\HordeServer on Windows, {AppDir}/Data on other platforms. /// public string? DataDir { get; set; } = null; /// /// Whether the server is running in 'installed' mode. In this mode, on Windows, the default data directory will use the common /// application data folder (C:\ProgramData\Epic\Horde), and configuration data will be read from here and the registry. /// This setting is overridden to false for local builds from appsettings.Local.json. /// public bool Installed { get; set; } = true; /// /// Main port for serving HTTP. /// public int HttpPort { get; set; } = 5000; /// /// Port for serving HTTP with TLS enabled. Disabled by default. /// public int HttpsPort { get; set; } = 0; /// /// Dedicated port for serving only HTTP/2. /// public int Http2Port { get; set; } = 5002; /// /// Connection string for the Mongo database /// public string? MongoConnectionString { get; set; } /// /// MongoDB connection string /// [Obsolete("Use MongoConnectionString instead")] public string? DatabaseConnectionString { get => MongoConnectionString; set => MongoConnectionString = value; } /// /// MongoDB database name /// public string MongoDatabaseName { get; set; } = "Horde"; /// [Obsolete("Replace references to DatabaseName with MongoDatabaseName")] public string DatabaseName { get => MongoDatabaseName; set => MongoDatabaseName = value; } /// /// Optional certificate to trust in order to access the database (eg. AWS public cert for TLS) /// public string? MongoPublicCertificate { get; set; } /// [Obsolete("Replace DatabasePublicCert with MongoPublicCertificate")] public string? DatabasePublicCert { get => MongoPublicCertificate; set => MongoPublicCertificate = value; } /// /// Access the database in read-only mode (avoids creating indices or updating content) /// Useful for debugging a local instance of HordeServer against a production database. /// public bool MongoReadOnlyMode { get; set; } = false; /// [Obsolete("Replace DatabaseReadOnlyMode with MongoReadOnlyMode")] public bool DatabaseReadOnlyMode { get => MongoReadOnlyMode; set => MongoReadOnlyMode = value; } /// /// Whether database schema migrations are enabled /// public bool MongoMigrationsEnabled { get; set; } = false; /// /// Whether database schema should automatically be applied /// Only recommended for dev or test environments /// public bool MongoMigrationsAutoUpgrade { get; set; } = false; /// /// Shutdown the current server process if memory usage reaches this threshold (specified in MB) /// /// Usually set to 80-90% of available memory to avoid CLR heap using all of it. /// If a memory leak was to occur, it's usually better to restart the process rather than to let the GC /// work harder and harder trying to recoup memory. /// /// Should only be used when multiple server processes are running behind a load balancer /// and one can be safely restarted automatically by the underlying process handler (Docker, Kubernetes, AWS ECS, Supervisor etc). /// The shutdown behaves similar to receiving a SIGTERM and will wait for outstanding requests to finish. /// public int? ShutdownMemoryThreshold { get; set; } = null; /// /// Optional PFX certificate to use for encrypting agent SSL traffic. This can be a self-signed certificate, as long as it's trusted by agents. /// public string? ServerPrivateCert { get; set; } /// /// Type of authentication (e.g anonymous, OIDC, built-in Horde accounts) /// If "Horde" auth mode is used, be sure to configure "ServerUrl" as well. /// /// public AuthMethod AuthMethod { get; set; } = AuthMethod.Anonymous; /// /// Optional profile name to report through the /api/v1/server/auth endpoint. Allows sharing auth tokens between providers configured through /// the same profile name in OidcToken.exe config files. /// public string? OidcProfileName { get; set; } /// /// OpenID Connect (OIDC) authority URL (required when OIDC is enabled) /// public string? OidcAuthority { get; set; } /// /// Audience for validating externally issued tokens (required when OIDC is enabled) /// public string? OidcAudience { get; set; } /// /// Client ID for the OIDC authority (required when OIDC is enabled) /// public string? OidcClientId { get; set; } /// /// Client secret for authenticating with the OIDC provider. /// Note: If you need authentication support in Unreal Build Tool or Unreal Game Sync, /// configure your OIDC client as a public client (using PKCE flow without a client secret) instead of a confidential client. /// These tools utilize the EpicGames.OIDC library which only supports public clients with authorization code flow + PKCE. /// public string? OidcClientSecret { get; set; } /// /// Optional redirect url provided to OIDC login /// public string? OidcSigninRedirect { get; set; } /// /// Optional redirect url provided to OIDC login for external tools (typically to a local server) /// Default value is the local web server started during signin by EpicGames.OIDC library /// public string[]? OidcLocalRedirectUrls { get; set; } = ["http://localhost:8749/ugs.client"]; /// /// Debug mode for OIDC which logs reasons for why JWT tokens fail to authenticate /// Also turns off HTTPS requirement for OIDC metadata fetching. /// NOT FOR PRODUCTION USE! /// public bool OidcDebugMode { get; set; } = false; /// /// OpenID Connect scopes to request when signing in /// public string[] OidcRequestedScopes { get; set; } = []; /// /// List of fields in /userinfo endpoint to try map to the standard name claim (see System.Security.Claims.ClaimTypes.Name) /// public string[] OidcClaimNameMapping { get; set; } = []; /// /// List of fields in /userinfo endpoint to try map to the standard email claim (see System.Security.Claims.ClaimTypes.Email) /// public string[] OidcClaimEmailMapping { get; set; } = []; /// /// List of fields in /userinfo endpoint to try map to the Horde user claim (see HordeClaimTypes.User) /// public string[] OidcClaimHordeUserMapping { get; set; } = []; /// /// List of fields in /userinfo endpoint to try map to the Horde Perforce user claim (see HordeClaimTypes.PerforceUser) /// public string[] OidcClaimHordePerforceUserMapping { get; set; } = []; /// /// API scopes to request when acquiring OIDC access tokens /// public string[] OidcApiRequestedScopes { get; set; } = []; /// /// Add common scopes and mappings to above OIDC config fields /// Provided as a workaround since .NET config will only *merge* array entries when combining multiple config sources. /// Due to this unwanted behavior, having hard-coded defaults makes such fields unchangeable. /// See https://github.com/dotnet/runtime/issues/36569 /// /// /// /// /// /// public bool OidcAddDefaultScopesAndMappings { get; set; } = true; /// /// Adds default scopes and claim mappings to the OIDC configuration if enabled. /// This method should be called after the initial configuration is loaded. /// public void AddDefaultOidcScopesAndMappings() { IReadOnlyList defaultScopes = ["profile", "email", "openid"]; IReadOnlyList defaultMappings = ["preferred_username", "email"]; IReadOnlyList defaultEmailMapping = ["email"]; IReadOnlyList defaultApiScopes = ["offline_access", "openid"]; if (OidcAddDefaultScopesAndMappings) { OidcRequestedScopes = OidcRequestedScopes.Concat(defaultScopes).ToArray(); OidcClaimNameMapping = OidcClaimNameMapping.Concat(defaultMappings).ToArray(); OidcClaimEmailMapping = OidcClaimEmailMapping.Concat(defaultEmailMapping).ToArray(); OidcClaimHordeUserMapping = OidcClaimHordeUserMapping.Concat(defaultMappings).ToArray(); OidcClaimHordePerforceUserMapping = OidcClaimHordePerforceUserMapping.Concat(defaultMappings).ToArray(); OidcApiRequestedScopes = OidcApiRequestedScopes.Concat(defaultApiScopes).ToArray(); } } /// /// Base URL this Horde server is accessible from /// For example https://horde.mystudio.com. If not set, a default is used based on current hostname. /// It's important this URL matches where users and agents access the server as it's used for signing auth tokens etc. /// Must be configured manually when running behind a reverse proxy or load balancer /// public Uri ServerUrl { get => _serverUrl ?? GetDefaultServerUrl(); set => _serverUrl = value; } /// /// Name of the issuer in bearer tokens from the server /// public string? JwtIssuer { get => _jwtIssuer ?? ServerUrl.ToString(); set => _jwtIssuer = value; } internal bool IsDefaultServerUrlUsed() =>_serverUrl == GetDefaultServerUrl(); Uri? _serverUrl; string? _jwtIssuer; Uri GetDefaultServerUrl() { string hostName = Dns.GetHostName(); if (HttpsPort == 443) { return new Uri($"https://{hostName}"); } else if (HttpsPort != 0) { return new Uri($"https://{hostName}:{HttpsPort}"); } else if (HttpPort == 80) { return new Uri($"http://{hostName}"); } else { return new Uri($"http://{hostName}:{HttpPort}"); } } /// /// Length of time before JWT tokens expire, in hours /// public int JwtExpiryTimeHours { get; set; } = 8; /// /// The claim type for administrators /// public string? AdminClaimType { get; set; } /// /// Value of the claim type for administrators /// public string? AdminClaimValue { get; set; } /// /// Whether to enable Cors, generally for development purposes /// public bool CorsEnabled { get; set; } = false; /// /// Allowed Cors origin /// public string CorsOrigin { get; set; } = null!; /// /// Whether to enable debug/administrative REST API endpoints /// public bool EnableDebugEndpoints { get; set; } = false; /// /// Whether to automatically enable new agents by default. If false, new agents must manually be enabled before they can take on work. /// public bool EnableNewAgentsByDefault { get; set; } = false; /// /// Interval between rebuilding the schedule queue with a DB query. /// public TimeSpan SchedulePollingInterval { get; set; } = TimeSpan.FromSeconds(60.0); /// /// Interval between polling for new jobs /// public TimeSpan NoResourceBackOffTime { get; set; } = TimeSpan.FromSeconds(30.0); /// /// Interval between attempting to assign agents to take on jobs /// public TimeSpan InitiateJobBackOffTime { get; set; } = TimeSpan.FromSeconds(180.0); /// /// Interval between scheduling jobs when an unknown error occurs /// public TimeSpan UnknownErrorBackOffTime { get; set; } = TimeSpan.FromSeconds(120.0); /// /// Config for connecting to Redis server(s). /// Setting it to null will disable Redis use and connection /// See format at https://stackexchange.github.io/StackExchange.Redis/Configuration.html /// public string? RedisConnectionString { get; set; } /// [Obsolete("Use RedisConnectionString instead")] public string? RedisConnectionConfig { get => RedisConnectionString; set => RedisConnectionString = value; } /// /// Whether to disable writes to Redis. /// public bool RedisReadOnlyMode { get; set; } /// /// Overridden settings for storage backends. Useful for running against a production server with custom backends. /// public string LogServiceWriteCacheType { get; set; } = "InMemory"; /// /// Whether to log json to stdout /// public bool LogJsonToStdOut { get; set; } = false; /// /// Whether to log requests to the UpdateSession and QueryServerState RPC endpoints /// public bool LogSessionRequests { get; set; } = false; /// /// Timezone for evaluating schedules /// public string? ScheduleTimeZone { get; set; } /// /// The URl to use for generating links back to the dashboard. /// public Uri DashboardUrl { get; set; } = new Uri("https://localhost:3000"); /// /// Help email address that users can contact with issues /// public string? HelpEmailAddress { get; set; } /// /// Help slack channel that users can use for issues /// public string? HelpSlackChannel { get; set; } /// /// Set the minimum size of the global thread pool /// This value has been found in need of tweaking to avoid timeouts with the Redis client during bursts /// of traffic. Default is 16 for .NET Core CLR. The correct value is dependent on the traffic the Horde Server /// is receiving. For Epic's internal deployment, this is set to 40. /// public int? GlobalThreadPoolMinSize { get; set; } /// /// Whether to enable Datadog integration for tracing /// public bool WithDatadog { get; set; } /// /// Path to the root config file. Relative to the server.json file by default. /// public string ConfigPath { get; set; } = "globals.json"; /// /// Forces configuration data to be read and updated as part of appplication startup, rather than on a schedule. Useful when running locally. /// public bool ForceConfigUpdateOnStartup { get; set; } /// /// Whether to open a browser on startup /// public bool OpenBrowser { get; set; } = false; /// /// Experimental features to enable on the server. /// public FeatureFlagSettings FeatureFlags { get; set; } = new(); /// /// Options for OpenTelemetry /// public OpenTelemetrySettings OpenTelemetry { get; set; } = new OpenTelemetrySettings(); /// /// Helper method to check if this process has activated the given mode /// /// Run mode /// True if mode is active public bool IsRunModeActive(RunMode mode) { if (RunModes == null) { return true; } return RunModes.Contains(mode); } /// /// Validate the settings object does not contain any invalid fields /// /// public void Validate(ILogger? logger = null) { if (RunModes != null && IsRunModeActive(RunMode.None)) { throw new ArgumentException($"Settings key '{nameof(RunModes)}' contains one or more invalid entries"); } if (AuthMethod != AuthMethod.Anonymous && IsDefaultServerUrlUsed() && logger != null) { logger.LogError("Configure {Setting} in server settings when non-anonymous auth method is used", nameof(ServerUrl)); } } } }