diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs
index 53509172bb..4dc3ef58be 100644
--- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs
+++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs
@@ -1,4 +1,7 @@
-namespace Tgstation.Server.Host.Configuration
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace Tgstation.Server.Host.Configuration
{
///
/// General configuration options
@@ -10,10 +13,20 @@
///
public const string Section = "General";
+ ///
+ /// The default value for
+ ///
+ const uint DefaultMinimumPasswordLength = 15;
+
+ ///
+ /// The default value for
+ ///
+ const int DefaultByondTopicTimeout = 5000;
+
///
/// Minimum length of database user passwords
///
- public uint MinimumPasswordLength { get; set; }
+ public uint MinimumPasswordLength { get; set; } = DefaultMinimumPasswordLength;
///
/// A GitHub personal access token to use for bypassing rate limits on requests. Requires no scopes
@@ -21,8 +34,14 @@
public string GitHubAccessToken { get; set; }
///
- /// If the should just check if the configuration wizard needs to be run and then exit
+ /// The
///
- public bool ConfigCheckOnly { get; set; }
+ [JsonConverter(typeof(StringEnumConverter))]
+ public SetupWizardMode SetupWizardMode { get; set; }
+
+ ///
+ /// The timeout in milliseconds for sending and receiving topics to/from DreamDaemon. Note that a single topic exchange can take up to twice this value
+ ///
+ public int ByondTopicTimeout { get; set; } = DefaultByondTopicTimeout;
}
}
diff --git a/src/Tgstation.Server.Host/Configuration/SetupWizardMode.cs b/src/Tgstation.Server.Host/Configuration/SetupWizardMode.cs
new file mode 100644
index 0000000000..4f2b59204a
--- /dev/null
+++ b/src/Tgstation.Server.Host/Configuration/SetupWizardMode.cs
@@ -0,0 +1,25 @@
+namespace Tgstation.Server.Host.Configuration
+{
+ ///
+ /// Determines if the will run
+ ///
+ public enum SetupWizardMode
+ {
+ ///
+ /// Run the wizard if the appsettings.{Environment}.json is not present or empty
+ ///
+ Autodetect,
+ ///
+ /// Force run the wizard
+ ///
+ Force,
+ ///
+ /// Only run the wizard and exit
+ ///
+ Only,
+ ///
+ /// Never run the wizard
+ ///
+ Never
+ }
+}
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index 31237dbbb7..c7b55d8bdc 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -10,22 +10,16 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
-using Microsoft.IdentityModel.Tokens;
-using MySql.Data.MySqlClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Display;
using System;
-using System.Collections.Generic;
-using System.Data;
-using System.Data.SqlClient;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Reflection;
using System.Runtime.InteropServices;
-using System.Text;
using System.Threading.Tasks;
using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Components.Byond;
@@ -82,383 +76,81 @@ namespace Tgstation.Server.Host.Core
VersionString = String.Format(CultureInfo.InvariantCulture, "{0} v{1}", VersionPrefix, Version);
}
- bool CheckRunSetupWizard(IIOManager ioManager)
- {
- if (!Environment.UserInteractive)
- return false;
- var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName);
- var existenceTask = ioManager.FileExists(userConfigFileName, default);
- var exists = existenceTask.GetAwaiter().GetResult();
-
- if (exists)
- {
- var readTask = ioManager.ReadAllBytes(userConfigFileName, default);
- var bytes = readTask.GetAwaiter().GetResult();
- var contents = Encoding.UTF8.GetString(bytes);
- if (!String.IsNullOrWhiteSpace(contents))
- return false;
- }
-
- //non-present or empty config json
- //make our own with blackjack and hookers
-
- Console.WriteLine("Welcome to tgstation-server 4!");
- Console.WriteLine("This wizard will help you configure your server.");
- Console.WriteLine();
- Console.WriteLine("What port would you like to connect to TGS on?");
- Console.WriteLine("Note: If this is a docker container with the default port already mapped, use the default.");
-
- ushort? port = null;
- do
- {
- Console.Write("API Port (leave blank for default): ");
- var portString = Console.ReadLine();
- if (String.IsNullOrWhiteSpace(portString))
- break;
- if (UInt16.TryParse(portString, out var concretePort) && concretePort != 0)
- {
- port = concretePort;
- break;
- }
- Console.WriteLine("Invalid port! Please enter a value between 1 and 65535");
- }
- while (true);
-
-
- string GetPassword()
- {
- var pwd = new StringBuilder();
- do
- {
- var i = Console.ReadKey(true);
- if (i.Key == ConsoleKey.Enter)
- break;
- else if (i.Key == ConsoleKey.Backspace)
- {
- if (pwd.Length > 0)
- {
- --pwd.Length;
- Console.Write("\b \b");
- }
- }
- else if (i.KeyChar != '\u0000') // KeyChar == '\u0000' if the key pressed does not correspond to a printable character, e.g. F1, Pause-Break, etc
- {
- pwd.Append(i.KeyChar);
- Console.Write("*");
- }
- }
- while (true);
- Console.WriteLine();
- return pwd.ToString();
- }
-
- DatabaseConfiguration databaseConfiguration;
- do
- {
- Console.WriteLine();
- Console.WriteLine("What SQL database type will you be using?");
-
- databaseConfiguration = new DatabaseConfiguration();
- do
- {
- Console.Write(String.Format(CultureInfo.InvariantCulture, "Please enter one of {0}, {1}, or {2}: ", DatabaseType.MariaDB, DatabaseType.SqlServer, DatabaseType.MySql));
- var databaseTypeString = Console.ReadLine();
- if (Enum.TryParse(databaseTypeString, out var databaseType))
- {
- databaseConfiguration.DatabaseType = databaseType;
- break;
- }
- Console.WriteLine("Invalid database type!");
- }
- while (true);
-
- Console.WriteLine();
- Console.Write("Enter the server's address and port (blank for local): ");
- var serverAddress = Console.ReadLine();
- if (String.IsNullOrWhiteSpace(serverAddress))
- serverAddress = null;
-
- Console.WriteLine();
- Console.Write("Enter the database name (Can be from previous installation. Otherwise, should not exist): ");
- var databaseName = Console.ReadLine();
-
- bool dbExists;
- do
- {
- Console.Write("Does this database already exist? (y/n): ");
- var responseString = Console.ReadLine();
- var upperResponse = responseString.ToUpperInvariant();
- if (upperResponse == "Y" || upperResponse == "YES")
- {
- dbExists = true;
- break;
- }
- else if (upperResponse == "N" || upperResponse == "NO")
- {
- dbExists = false;
- break;
- }
- Console.WriteLine("Invalid response!");
- }
- while (true);
-
- bool? useWinAuth;
- if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- do
- {
- Console.Write("Use Windows Authentication? (y/n): ");
- var responseString = Console.ReadLine();
- var upperResponse = responseString.ToUpperInvariant();
- if (upperResponse == "Y" || upperResponse == "YES")
- {
- useWinAuth = true;
- break;
- }
- else if (upperResponse == "N" || upperResponse == "NO")
- {
- useWinAuth = false;
- break;
- }
- Console.WriteLine("Invalid response!");
- }
- while (true);
- else
- useWinAuth = null;
-
- Console.WriteLine();
-
- string username = null;
- string password = null;
- if (useWinAuth != true)
- {
- Console.Write("Enter username: ");
- username = Console.ReadLine();
- Console.Write("Enter password: ");
- password = GetPassword();
- Console.WriteLine();
- }
-
- IDbConnection testConnection;
- if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
- {
- var csb = new SqlConnectionStringBuilder
- {
- ApplicationName = VersionPrefix,
- DataSource = serverAddress ?? "(local)"
- };
- if (useWinAuth.Value)
- csb.IntegratedSecurity = true;
- else
- {
- csb.UserID = username;
- csb.Password = password;
- }
- testConnection = new SqlConnection
- {
- ConnectionString = csb.ConnectionString
- };
- csb.InitialCatalog = databaseName;
- databaseConfiguration.ConnectionString = csb.ConnectionString;
- }
- else
- {
- var csb = new MySqlConnectionStringBuilder
- {
- Server = serverAddress ?? "127.0.0.1",
- UserID = username,
- Password = password
- };
- testConnection = new MySqlConnection
- {
- ConnectionString = csb.ConnectionString
- };
- csb.Database = databaseName;
- databaseConfiguration.ConnectionString = csb.ConnectionString;
- }
-
- try
- {
- using (testConnection)
- {
- Console.WriteLine("Testing connection...");
- testConnection.Open();
- Console.WriteLine("Connection successful!");
-
- if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer)
- {
- Console.WriteLine("Checking MySQL/MariaDB version...");
- using (var command = testConnection.CreateCommand())
- {
- command.CommandText = "SELECT VERSION()";
- var fullVersion = (string)command.ExecuteScalar();
- Console.WriteLine(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion));
- var splits = fullVersion.Split('-');
- databaseConfiguration.MySqlServerVersion = splits[0];
- }
- }
-
- if (!dbExists)
- {
- Console.WriteLine("Testing create DB permission...");
- using (var command = testConnection.CreateCommand())
- {
- command.CommandText = String.Format(CultureInfo.InvariantCulture, "CREATE DATABASE {0}", databaseName);
- command.ExecuteNonQuery();
- }
- Console.WriteLine("Success!");
- Console.WriteLine("Dropping test database...");
- using (var command = testConnection.CreateCommand())
- {
- command.CommandText = String.Format(CultureInfo.InvariantCulture, "DROP DATABASE {0}", databaseName);
- try
- {
- command.ExecuteNonQuery();
- }
- catch (Exception e)
- {
- Console.WriteLine(e.Message);
- Console.WriteLine();
- Console.WriteLine("This should be okay, but you may want to manually drop the database before continuing!");
- Console.WriteLine("Press any key to continue...");
- Console.ReadKey();
- }
- }
- }
- }
-
- break;
- }
- catch (Exception e)
- {
- Console.WriteLine(e.Message);
- Console.WriteLine();
- Console.WriteLine("Retrying database configuration...");
- }
- } while (true);
-
- var generalConfiguration = new GeneralConfiguration
- {
- MinimumPasswordLength = 15
- };
- do
- {
- Console.WriteLine();
- Console.Write("Minimum database user password length (leave blank for default of 15): ");
- var passwordLengthString = Console.ReadLine();
- if (String.IsNullOrWhiteSpace(passwordLengthString))
- break;
- if (UInt32.TryParse(passwordLengthString, out var passwordLength))
- {
- generalConfiguration.MinimumPasswordLength = passwordLength;
- break;
- }
- Console.WriteLine("Please enter a positive integer!");
- }
- while (true);
-
- Console.WriteLine();
- Console.WriteLine("Enter a GitHub personal access token to bypass some rate limits (this is optional and does not require any scopes)");
- Console.Write("GitHub personal access token: ");
- generalConfiguration.GitHubAccessToken = GetPassword();
- if (String.IsNullOrWhiteSpace(generalConfiguration.GitHubAccessToken))
- generalConfiguration.GitHubAccessToken = null;
-
- Console.WriteLine();
- Console.WriteLine(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName));
-
- var map = new Dictionary()
- {
- { DatabaseConfiguration.Section, databaseConfiguration },
- { GeneralConfiguration.Section, generalConfiguration }
- };
-
- if (port.HasValue)
- map.Add("Kestrel", new {
- EndPoints = new
- {
- Http = new
- {
- Url = String.Format(CultureInfo.InvariantCulture, "http://0.0.0.0:{0}", port)
- }
- }
- });
-
- var json = JsonConvert.SerializeObject(map, Formatting.Indented);
- var configBytes = Encoding.UTF8.GetBytes(json);
-
- var writeTask = ioManager.WriteAllBytes(userConfigFileName, configBytes, default);
- try
- {
- writeTask.GetAwaiter().GetResult();
- }
- catch (Exception e)
- {
- Console.WriteLine(e.Message);
- Console.WriteLine();
- Console.WriteLine("For your convienence, here's the text we tried to write out:");
- Console.WriteLine();
- Console.WriteLine(json);
- Console.WriteLine();
- Console.WriteLine("Press any key to exit...");
- Console.ReadKey();
- throw new OperationCanceledException();
- }
-
- Console.WriteLine("Waiting for configuration changes to reload...");
- Task.Delay(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult();
-
- return true;
- }
-
///
/// Configure dependency injected services
///
/// The to configure
+ /// The representing the lifetime of the
public void ConfigureServices(IServiceCollection services)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
+ //needful
+ services.AddSingleton(this);
+
+ //configure configuration
services.Configure(configuration.GetSection(UpdatesConfiguration.Section));
services.Configure(configuration.GetSection(DatabaseConfiguration.Section));
services.Configure(configuration.GetSection(GeneralConfiguration.Section));
services.Configure(configuration.GetSection(FileLoggingConfiguration.Section));
+ //enable options which give us config reloading
services.AddOptions();
+ //need this instance for later configuration
var ioManager = new DefaultIOManager();
- var ranWizard = CheckRunSetupWizard(ioManager);
+
+ //setup stuff for setup wizard
+ services.AddSingleton(ioManager);
+ services.AddSingleton();
+ services.AddSingleton();
+
+ //needed here for JWT configuration
+ services.AddSingleton();
GeneralConfiguration generalConfiguration;
DatabaseConfiguration databaseConfiguration;
FileLoggingConfiguration fileLoggingConfiguration;
+ ITokenFactory tokenFactory;
+
+ //temporarily build the service provider in it's current state
+ //do it here so we can run the setup wizard if necessary
+ //also allows us to get some options and other services we need for continued configuration
using (var provider = services.BuildServiceProvider())
{
+ //run the wizard if necessary
+ var setupWizard = provider.GetRequiredService();
+ var applicationLifetime = provider.GetRequiredService();
+ var setupWizardRan = setupWizard.CheckRunWizard(applicationLifetime.ApplicationStopping).GetAwaiter().GetResult();
+
+ //load the configuration options we need
var generalOptions = provider.GetRequiredService>();
generalConfiguration = generalOptions.Value;
- if (generalConfiguration.ConfigCheckOnly)
- throw new OperationCanceledException("Configuration check complete!");
-
- if (ranWizard)
- Console.WriteLine("Now launching TGS...");
+ //unless this is set, in which case, we leave
+ if (setupWizardRan && generalConfiguration.SetupWizardMode == SetupWizardMode.Only)
+ //we don't inject a logger in the constuctor to log this because it's not yet configured
+ throw new OperationCanceledException("Exiting due to SetupWizardMode configuration!");
var dbOptions = provider.GetRequiredService>();
databaseConfiguration = dbOptions.Value;
var loggingOptions = provider.GetRequiredService>();
fileLoggingConfiguration = loggingOptions.Value;
+
+ tokenFactory = provider.GetRequiredService();
}
-
+ //setup file logging via serilog
if (!fileLoggingConfiguration.Disable)
- {
- var logPath = !String.IsNullOrEmpty(fileLoggingConfiguration.Directory) ? fileLoggingConfiguration.Directory : ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), VersionPrefix, "Logs");
-
- logPath = ioManager.ConcatPath(logPath, "tgs-{Date}.log");
-
services.AddLogging(builder =>
{
+ //common app data is C:/ProgramData on windows, else /usr/shar
+ var logPath = !String.IsNullOrEmpty(fileLoggingConfiguration.Directory) ? fileLoggingConfiguration.Directory : ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), VersionPrefix, "Logs");
+
+ logPath = ioManager.ConcatPath(logPath, "tgs-{Date}.log");
+
LogEventLevel? ConvertLogLevel(LogLevel logLevel)
{
switch (logLevel)
@@ -499,44 +191,21 @@ namespace Tgstation.Server.Host.Core
builder.AddSerilog(configuration.CreateLogger(), true);
});
- }
- services.AddScoped();
-
- const string scheme = "JwtBearer";
- services.AddAuthentication((options) =>
+ //configure bearer token validation
+ services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions =>
{
- options.DefaultAuthenticateScheme = scheme;
- options.DefaultChallengeScheme = scheme;
- }).AddJwtBearer(scheme, jwtBearerOptions =>
- {
- jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
- {
- ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(TokenFactory.TokenSigningKey),
-
- ValidateIssuer = true,
- ValidIssuer = TokenFactory.TokenIssuer,
-
- ValidateLifetime = true,
- ValidateAudience = true,
- ValidAudience = TokenFactory.TokenAudience,
-
- ClockSkew = TimeSpan.FromMinutes(1),
-
- RequireSignedTokens = true,
-
- RequireExpirationTime = true
- };
+ jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
jwtBearerOptions.Events = new JwtBearerEvents
{
//Application is our composition root so this monstrosity of a line is okay
OnTokenValidated = ctx => ctx.HttpContext.RequestServices.GetRequiredService().InjectClaimsIntoContext(ctx, ctx.HttpContext.RequestAborted)
};
+
+ JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //fucking converts 'sub' to M$ bs
});
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //fucking converts 'sub' to M$ bs
-
+ //add mvc, configure the json serializer settings
services.AddMvc().AddJsonOptions(options =>
{
options.AllowInputFormatterExceptionMessages = true;
@@ -547,7 +216,6 @@ namespace Tgstation.Server.Host.Core
options.SerializerSettings.Converters = new[] { new VersionConverter() };
});
-
void AddTypedContext() where TContext : DatabaseContext
{
services.AddDbContext(builder =>
@@ -558,6 +226,7 @@ namespace Tgstation.Server.Host.Core
services.AddScoped(x => x.GetRequiredService());
}
+ //add the correct database context type
var dbType = databaseConfiguration.DatabaseType;
switch (dbType)
{
@@ -572,18 +241,18 @@ namespace Tgstation.Server.Host.Core
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
}
- services.AddScoped();
- services.AddSingleton();
-
- services.AddSingleton();
+ //configure other database services
+ services.AddSingleton();
services.AddSingleton();
+
+ //configure security services
+ services.AddScoped();
+ services.AddScoped();
+ services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton, PasswordHasher>();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
-
- services.AddSingleton();
+ //configure platform specific services
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
services.AddSingleton();
@@ -604,30 +273,29 @@ namespace Tgstation.Server.Host.Core
services.AddSingleton();
}
+ //configure misc services
+ services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
services.AddSingleton(new ByondTopicSender
{
- ReceiveTimeout = 5000,
- SendTimeout = 5000
+ ReceiveTimeout = generalConfiguration.ByondTopicTimeout,
+ SendTimeout = generalConfiguration.ByondTopicTimeout
});
+ //configure component services
+ services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ //configure root services
services.AddSingleton();
services.AddSingleton(x => x.GetRequiredService());
services.AddSingleton(x => x.GetRequiredService());
services.AddSingleton();
-
- services.AddSingleton(ioManager);
-
- services.AddSingleton();
- services.AddSingleton(x => x.GetRequiredService());
-
- services.AddSingleton(this);
}
///
@@ -650,21 +318,31 @@ namespace Tgstation.Server.Host.Core
//attempt to restart the server if the configuration changes
ChangeToken.OnChange(configuration.GetReloadToken, () => serverControl.Restart());
+ //now setup the HTTP request pipeline
+
+ //should anything after this throw an exception, catch it and display a detailed html page
applicationBuilder.UseDeveloperExceptionPage(); //it is not worth it to limit this, you should only ever get it if you're an authorized user
+ //suppress OperationCancelledExceptions, they are just aborted HTTP requests
applicationBuilder.UseCancelledRequestSuppression();
+ //Do not service requests until Ready is called, this will return 503 until that point
applicationBuilder.UseAsyncInitialization(async cancellationToken =>
{
using (cancellationToken.Register(() => startupTcs.SetCanceled()))
await startupTcs.Task.ConfigureAwait(false);
});
+ //authenticate JWT tokens using our security pipeline if present, returns 401 if bad
applicationBuilder.UseAuthentication();
+ //suppress and log database exceptions
applicationBuilder.UseDbConflictHandling();
+ //majority of handling is done in the controllers
applicationBuilder.UseMvc();
+
+ //404 anything that gets this far
}
///
diff --git a/src/Tgstation.Server.Host/Core/ISetupWizard.cs b/src/Tgstation.Server.Host/Core/ISetupWizard.cs
new file mode 100644
index 0000000000..bf8ddae948
--- /dev/null
+++ b/src/Tgstation.Server.Host/Core/ISetupWizard.cs
@@ -0,0 +1,18 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.Core
+{
+ ///
+ /// The command line setup wizard
+ ///
+ interface ISetupWizard
+ {
+ ///
+ /// Run the setup wizard if necessary
+ ///
+ ///
+ ///
+ Task CheckRunWizard(CancellationToken cancellationToken);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Core/SetupWizard.cs b/src/Tgstation.Server.Host/Core/SetupWizard.cs
new file mode 100644
index 0000000000..182376fea4
--- /dev/null
+++ b/src/Tgstation.Server.Host/Core/SetupWizard.cs
@@ -0,0 +1,471 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using MySql.Data.MySqlClient;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Data.SqlClient;
+using System.Globalization;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.IO;
+
+namespace Tgstation.Server.Host.Core
+{
+ ///
+ sealed class SetupWizard : ISetupWizard
+ {
+ ///
+ /// The for the
+ ///
+ readonly IIOManager ioManager;
+
+ ///
+ /// The for the
+ ///
+ readonly IConsole console;
+
+ ///
+ /// The for the
+ ///
+ readonly IHostingEnvironment hostingEnvironment;
+
+ ///
+ /// The for the
+ ///
+ readonly IApplication application;
+
+ ///
+ /// The for the
+ ///
+ readonly ILogger logger;
+
+ ///
+ /// The for the
+ ///
+ readonly GeneralConfiguration generalConfiguration;
+
+ ///
+ /// Construct a
+ ///
+ /// The value of
+ /// The value of
+ /// The value of
+ /// The value of
+ /// The containing the value of
+ public SetupWizard(IIOManager ioManager, IConsole console, IHostingEnvironment hostingEnvironment, IApplication application, ILogger logger, IOptions generalConfigurationOptions)
+ {
+ this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
+ this.console = console ?? throw new ArgumentNullException(nameof(console));
+ this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
+ this.application = application ?? throw new ArgumentNullException(nameof(application));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
+ }
+
+ ///
+ /// A prompt for a yes or no value
+ ///
+ /// The question
+ /// The for the operation
+ /// A resulting in if the user replied yes, otherwise
+ async Task PromptYesNo(string question, CancellationToken cancellationToken)
+ {
+ do
+ {
+ await console.WriteAsync(question, false, cancellationToken).ConfigureAwait(false);
+ var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
+ var upperResponse = responseString.ToUpperInvariant();
+ if (upperResponse == "Y" || upperResponse == "YES")
+ return true;
+ else if (upperResponse == "N" || upperResponse == "NO")
+ return false;
+ await console.WriteAsync("Invalid response!", true, cancellationToken).ConfigureAwait(false);
+ }
+ while (true);
+ }
+
+ ///
+ /// Prompts the user to enter the port to host TGS on
+ ///
+ /// The for the operation
+ /// A resulting in the hosting port, or to use the default
+ async Task PromptForHostingPort(CancellationToken cancellationToken)
+ {
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("What port would you like to connect to TGS on?", true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("Note: If this is a docker container with the default port already mapped, use the default.", true, cancellationToken).ConfigureAwait(false);
+
+ do
+ {
+ await console.WriteAsync("API Port (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
+ var portString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
+ if (String.IsNullOrWhiteSpace(portString))
+ return null;
+ if (UInt16.TryParse(portString, out var port) && port != 0)
+ return port;
+ await console.WriteAsync("Invalid port! Please enter a value between 1 and 65535", true, cancellationToken).ConfigureAwait(false);
+ }
+ while (true);
+ }
+
+ ///
+ /// Prompts the user to create a
+ ///
+ /// The for the operation
+ /// A resulting in the new
+ async Task ConfigureDatabase(CancellationToken cancellationToken)
+ {
+ do
+ {
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false);
+
+ var databaseConfiguration = new DatabaseConfiguration();
+ do
+ {
+ await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Please enter one of {0}, {1}, or {2}: ", DatabaseType.MariaDB, DatabaseType.SqlServer, DatabaseType.MySql), false, cancellationToken).ConfigureAwait(false);
+ var databaseTypeString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
+ if (Enum.TryParse(databaseTypeString, out var databaseType))
+ {
+ databaseConfiguration.DatabaseType = databaseType;
+ break;
+ }
+ await console.WriteAsync("Invalid database type!", true, cancellationToken).ConfigureAwait(false);
+ }
+ while (true);
+
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("Enter the server's address and port (blank for local): ", false, cancellationToken).ConfigureAwait(false);
+ var serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
+ if (String.IsNullOrWhiteSpace(serverAddress))
+ serverAddress = null;
+
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("Enter the database name (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false);
+ var databaseName = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
+
+ bool dbExists;
+ do
+ {
+ await console.WriteAsync("Does this database already exist? (y/n): ", false, cancellationToken).ConfigureAwait(false);
+ var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
+ var upperResponse = responseString.ToUpperInvariant();
+ if (upperResponse == "Y" || upperResponse == "YES")
+ {
+ dbExists = true;
+ break;
+ }
+ else if (upperResponse == "N" || upperResponse == "NO")
+ {
+ dbExists = false;
+ break;
+ }
+ await console.WriteAsync("Invalid response!", true, cancellationToken).ConfigureAwait(false);
+ }
+ while (true);
+
+ bool? useWinAuth;
+ if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken).ConfigureAwait(false);
+ else
+ useWinAuth = null;
+
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+
+ string username = null;
+ string password = null;
+ if (useWinAuth != true)
+ {
+ await console.WriteAsync("Enter username: ", false, cancellationToken).ConfigureAwait(false);
+ username = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("Enter password: ", false, cancellationToken).ConfigureAwait(false);
+ password = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ }
+
+ DbConnection testConnection;
+ if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
+ {
+ var csb = new SqlConnectionStringBuilder
+ {
+ ApplicationName = application.VersionPrefix,
+ DataSource = serverAddress ?? "(local)"
+ };
+ if (useWinAuth.Value)
+ csb.IntegratedSecurity = true;
+ else
+ {
+ csb.UserID = username;
+ csb.Password = password;
+ }
+ testConnection = new SqlConnection
+ {
+ ConnectionString = csb.ConnectionString
+ };
+
+ csb.InitialCatalog = databaseName;
+ databaseConfiguration.ConnectionString = csb.ConnectionString;
+ }
+ else
+ {
+ var csb = new MySqlConnectionStringBuilder
+ {
+ Server = serverAddress ?? "127.0.0.1",
+ UserID = username,
+ Password = password
+ };
+ testConnection = new MySqlConnection
+ {
+ ConnectionString = csb.ConnectionString
+ };
+ csb.Database = databaseName;
+ databaseConfiguration.ConnectionString = csb.ConnectionString;
+ }
+
+ try
+ {
+ using (testConnection)
+ {
+ await console.WriteAsync("Testing connection...", true, cancellationToken).ConfigureAwait(false);
+ await testConnection.OpenAsync(cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("Connection successful!", true, cancellationToken).ConfigureAwait(false);
+
+ if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer)
+ {
+ await console.WriteAsync("Checking MySQL/MariaDB version...", true, cancellationToken).ConfigureAwait(false);
+ using (var command = testConnection.CreateCommand())
+ {
+ command.CommandText = "SELECT VERSION()";
+ var fullVersion = (string)(await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false));
+ await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken).ConfigureAwait(false);
+ var splits = fullVersion.Split('-');
+ databaseConfiguration.MySqlServerVersion = splits[0];
+ }
+ }
+
+ if (!dbExists)
+ {
+ await console.WriteAsync("Testing create DB permission...", true, cancellationToken).ConfigureAwait(false);
+ using (var command = testConnection.CreateCommand())
+ {
+ command.CommandText = String.Format(CultureInfo.InvariantCulture, "CREATE DATABASE {0}", databaseName);
+ await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
+ }
+ await console.WriteAsync("Success!", true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("Dropping test database...", true, cancellationToken).ConfigureAwait(false);
+ using (var command = testConnection.CreateCommand())
+ {
+ command.CommandText = String.Format(CultureInfo.InvariantCulture, "DROP DATABASE {0}", databaseName);
+ try
+ {
+ await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("This should be okay, but you may want to manually drop the database before continuing!", true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("Press any key to continue...", true, cancellationToken).ConfigureAwait(false);
+ await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+
+ return databaseConfiguration;
+ }
+ catch (Exception e)
+ {
+ await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("Retrying database configuration...", true, cancellationToken).ConfigureAwait(false);
+ }
+ } while (true);
+ }
+
+ ///
+ /// Prompts the user to create a
+ ///
+ /// The for the operation
+ /// A resulting in the new
+ async Task ConfigureGeneral(CancellationToken cancellationToken)
+ {
+ var generalConfiguration = new GeneralConfiguration();
+ do
+ {
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank for default of {0}): ", generalConfiguration.MinimumPasswordLength), false, cancellationToken).ConfigureAwait(false);
+ var passwordLengthString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
+ if (String.IsNullOrWhiteSpace(passwordLengthString))
+ break;
+ if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0)
+ {
+ generalConfiguration.MinimumPasswordLength = passwordLength;
+ break;
+ }
+ await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
+ }
+ while (true);
+
+ do
+ {
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Timeout for sending and receiving BYOND topics (ms, 0 for infinite, leave blank for default of {0}): ", generalConfiguration.ByondTopicTimeout), false, cancellationToken).ConfigureAwait(false);
+ var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
+ if (String.IsNullOrWhiteSpace(topicTimeoutString))
+ break;
+ if (Int32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0)
+ {
+ generalConfiguration.ByondTopicTimeout = topicTimeout;
+ break;
+ }
+ await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
+ }
+ while (true);
+
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("Enter a GitHub personal access token to bypass some rate limits (this is optional and does not require any scopes)", true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("GitHub personal access token: ", false, cancellationToken).ConfigureAwait(false);
+ generalConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
+ if (String.IsNullOrWhiteSpace(generalConfiguration.GitHubAccessToken))
+ generalConfiguration.GitHubAccessToken = null;
+ return generalConfiguration;
+ }
+
+ async Task SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration generalConfiguration, CancellationToken cancellationToken)
+ {
+ await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
+
+ var map = new Dictionary()
+ {
+ { DatabaseConfiguration.Section, databaseConfiguration },
+ { GeneralConfiguration.Section, generalConfiguration }
+ };
+
+ if (hostingPort.HasValue)
+ map.Add("Kestrel", new
+ {
+ EndPoints = new
+ {
+ Http = new
+ {
+ Url = String.Format(CultureInfo.InvariantCulture, "http://0.0.0.0:{0}", hostingPort)
+ }
+ }
+ });
+
+ var json = JsonConvert.SerializeObject(map, Formatting.Indented);
+ var configBytes = Encoding.UTF8.GetBytes(json);
+
+ try
+ {
+ await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("For your convienence, here's the json we tried to write out:", true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync(json, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("Press any key to exit...", true, cancellationToken).ConfigureAwait(false);
+ await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
+ throw new OperationCanceledException();
+ }
+
+ await console.WriteAsync("Waiting for configuration changes to reload...", true, cancellationToken).ConfigureAwait(false);
+
+ //we need to wait for the configuration's file system watcher to read and reload the changes
+ await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Runs the
+ ///
+ /// The path to the settings json to build
+ /// The for the operation
+ ///
+ async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
+ {
+ //welcome message
+ await console.WriteAsync("Welcome to tgstation-server 4!", true, cancellationToken).ConfigureAwait(false);
+ await console.WriteAsync("This wizard will help you configure your server.", true, cancellationToken).ConfigureAwait(false);
+
+ var hostingPort = await PromptForHostingPort(cancellationToken).ConfigureAwait(false);
+
+ var databaseConfiguration = await ConfigureDatabase(cancellationToken).ConfigureAwait(false);
+
+ var generalConfiguration = await ConfigureGeneral(cancellationToken).ConfigureAwait(false);
+
+ await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
+
+ await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, generalConfiguration, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task CheckRunWizard(CancellationToken cancellationToken)
+ {
+ var setupWizardMode = generalConfiguration.SetupWizardMode;
+ logger.LogTrace("Checking if setup wizard should run. SetupWizardMode: {0}", setupWizardMode);
+
+ if (setupWizardMode == SetupWizardMode.Never)
+ {
+ logger.LogTrace("Skipping due to configuration...");
+ return false;
+ }
+
+ var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode == SetupWizardMode.Only;
+ if (!console.Available)
+ {
+ if (forceRun)
+ throw new InvalidOperationException("Asked to run setup wizard with no console avaliable!");
+ logger.LogTrace("Skipping due to console not being available...");
+ return false;
+ }
+
+ var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName);
+ var existenceTask = ioManager.FileExists(userConfigFileName, default);
+ var exists = existenceTask.GetAwaiter().GetResult();
+
+ bool shouldRunBasedOnAutodetect;
+ if (exists)
+ {
+ var readTask = ioManager.ReadAllBytes(userConfigFileName, default);
+ var bytes = readTask.GetAwaiter().GetResult();
+ var contents = Encoding.UTF8.GetString(bytes);
+ var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents);
+ logger.LogTrace("Configuration json detected. Empty: {0}", existingConfigIsEmpty);
+ shouldRunBasedOnAutodetect = existingConfigIsEmpty;
+ }
+ else
+ {
+ shouldRunBasedOnAutodetect = true;
+ logger.LogTrace("No configuration json detected");
+ }
+
+
+ if (!shouldRunBasedOnAutodetect)
+ {
+ if (forceRun)
+ {
+ logger.LogTrace("Asking user to bypass due to force run request...");
+ await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "The configuration settings are requesting the setup wizard be run, but you already appear to have a configuration file ({0})!", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
+
+ forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false);
+ }
+ if (!forceRun)
+ return false;
+ }
+
+ await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false);
+ return true;
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/IO/Console.cs b/src/Tgstation.Server.Host/IO/Console.cs
new file mode 100644
index 0000000000..5c262b9f7e
--- /dev/null
+++ b/src/Tgstation.Server.Host/IO/Console.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.IO
+{
+ ///
+ sealed class Console : IConsole
+ {
+ ///
+ public bool Available => Environment.UserInteractive;
+
+ ///
+ public Task PressAnyKeyAsync(CancellationToken cancellationToken) => Task.Factory.StartNew(() => System.Console.ReadKey(), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
+
+ ///
+ public Task ReadLineAsync(bool usePasswordChar, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
+ {
+ //TODO Make this better: https://stackoverflow.com/questions/9479573/how-to-interrupt-console-readline
+ if (!usePasswordChar)
+ return System.Console.ReadLine();
+
+ var passwordBuilder = new StringBuilder();
+ do
+ {
+ var keyDescription = System.Console.ReadKey(true);
+ if (keyDescription.Key == ConsoleKey.Enter)
+ break;
+ else if (keyDescription.Key == ConsoleKey.Backspace)
+ {
+ if (passwordBuilder.Length > 0)
+ {
+ --passwordBuilder.Length;
+ System.Console.Write("\b \b");
+ }
+ }
+ else if (keyDescription.KeyChar != '\u0000') // KeyChar == '\u0000' if the key pressed does not correspond to a printable character, e.g. F1, Pause-Break, etc
+ {
+ passwordBuilder.Append(keyDescription.KeyChar);
+ System.Console.Write('*');
+ }
+ }
+ while (!cancellationToken.IsCancellationRequested);
+ cancellationToken.ThrowIfCancellationRequested();
+ System.Console.WriteLine();
+ return passwordBuilder.ToString();
+ }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
+
+ ///
+ public Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
+ {
+ if (newLine)
+ System.Console.WriteLine(text);
+ else
+ System.Console.Write(text);
+ }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
+ }
+}
diff --git a/src/Tgstation.Server.Host/IO/IConsole.cs b/src/Tgstation.Server.Host/IO/IConsole.cs
new file mode 100644
index 0000000000..119b19d5dc
--- /dev/null
+++ b/src/Tgstation.Server.Host/IO/IConsole.cs
@@ -0,0 +1,40 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.IO
+{
+ ///
+ /// Abstraction for
+ ///
+ interface IConsole
+ {
+ ///
+ /// If the is visible to the user
+ ///
+ bool Available { get; }
+
+ ///
+ /// Write some to the
+ ///
+ /// The to write
+ /// If there should be a new line after the
+ /// The for the operation
+ /// A representing the running operation
+ Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken);
+
+ ///
+ /// Wait for a key press on the
+ ///
+ /// The for the operations
+ /// A representing the running operation
+ Task PressAnyKeyAsync(CancellationToken cancellationToken);
+
+ ///
+ /// Read a line from the
+ ///
+ /// If the input should be retrieved using the '*' character
+ /// The for the operation
+ /// A resulting in the read by the
+ Task ReadLineAsync(bool usePasswordChar, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Security/ITokenFactory.cs b/src/Tgstation.Server.Host/Security/ITokenFactory.cs
index cd67f0d876..0a459aaea1 100644
--- a/src/Tgstation.Server.Host/Security/ITokenFactory.cs
+++ b/src/Tgstation.Server.Host/Security/ITokenFactory.cs
@@ -1,4 +1,4 @@
-using System;
+using Microsoft.IdentityModel.Tokens;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Host.Security
@@ -8,6 +8,11 @@ namespace Tgstation.Server.Host.Security
///
public interface ITokenFactory
{
+ ///
+ /// The for the
+ ///
+ TokenValidationParameters ValidationParameters { get; }
+
///
/// Create a for a given
///
diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs
index f9eb4d0f85..5b33554c40 100644
--- a/src/Tgstation.Server.Host/Security/TokenFactory.cs
+++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs
@@ -16,9 +16,33 @@ namespace Tgstation.Server.Host.Security
///
const int TokenExpiryMinutes = 15;
- public static readonly string TokenAudience = typeof(Token).Assembly.GetName().Name;
- public static readonly string TokenIssuer = Assembly.GetExecutingAssembly().GetName().Name;
- public static readonly byte[] TokenSigningKey = CryptographySuite.GetSecureBytes(256);
+ ///
+ public TokenValidationParameters ValidationParameters { get; }
+
+ ///
+ /// Construct a
+ ///
+ public TokenFactory()
+ {
+ ValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = new SymmetricSecurityKey(CryptographySuite.GetSecureBytes(256)),
+
+ ValidateIssuer = true,
+ ValidIssuer = Assembly.GetExecutingAssembly().GetName().Name,
+
+ ValidateLifetime = true,
+ ValidateAudience = true,
+ ValidAudience = typeof(Token).Assembly.GetName().Name,
+
+ ClockSkew = TimeSpan.FromMinutes(1),
+
+ RequireSignedTokens = true,
+
+ RequireExpirationTime = true
+ };
+ }
///
public Token CreateToken(Models.User user)
@@ -32,13 +56,11 @@ namespace Tgstation.Server.Host.Security
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString(CultureInfo.InvariantCulture)),
new Claim(JwtRegisteredClaimNames.Exp, $"{expiry.ToUnixTimeSeconds()}"),
new Claim(JwtRegisteredClaimNames.Nbf, $"{DateTimeOffset.Now.ToUnixTimeSeconds()}"),
- new Claim(JwtRegisteredClaimNames.Iss, TokenIssuer),
- new Claim(JwtRegisteredClaimNames.Aud, TokenAudience)
+ new Claim(JwtRegisteredClaimNames.Iss, ValidationParameters.ValidIssuer),
+ new Claim(JwtRegisteredClaimNames.Aud, ValidationParameters.ValidAudience)
};
- var key = new SymmetricSecurityKey(TokenSigningKey);
-
- var token = new JwtSecurityToken(new JwtHeader(new SigningCredentials(key, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims));
+ var token = new JwtSecurityToken(new JwtHeader(new SigningCredentials(ValidationParameters.IssuerSigningKey, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims));
return new Token { Bearer = new JwtSecurityTokenHandler().WriteToken(token), ExpiresAt = expiry };
}
}
diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json
index 51c0b4b686..2da5abc3b2 100644
--- a/src/Tgstation.Server.Host/appsettings.json
+++ b/src/Tgstation.Server.Host/appsettings.json
@@ -2,7 +2,8 @@
"General": {
"MinimumPasswordLength": 15,
"GitHubAccessToken": null,
- "ConfigCheckOnly": false
+ "SetupWizardMode": "AutoDetect",
+ "ByondTopicTimeout": 5000
},
"FileLogging": {
"Directory": null, //use the default path