tgstation-server  4.4.0
The /tg/station 13 server suite
Application.cs
Go to the documentation of this file.
1 using Cyberboss.AspNetCore.AsyncInitializer;
2 using Microsoft.AspNetCore.Authentication.JwtBearer;
3 using Microsoft.AspNetCore.Builder;
4 using Microsoft.AspNetCore.Cors.Infrastructure;
5 using Microsoft.AspNetCore.Hosting;
6 using Microsoft.AspNetCore.Identity;
7 using Microsoft.Extensions.Configuration;
8 using Microsoft.Extensions.DependencyInjection;
9 using Microsoft.Extensions.Hosting;
10 using Microsoft.Extensions.Logging;
11 using Microsoft.Extensions.Options;
12 using Microsoft.Extensions.Primitives;
13 using Newtonsoft.Json;
14 using Serilog;
15 using Serilog.Events;
16 using Serilog.Formatting.Display;
17 using System;
18 using System.Globalization;
19 using System.IdentityModel.Tokens.Jwt;
20 using System.Threading.Tasks;
21 using Tgstation.Server.Api;
35 using Tgstation.Server.Host.IO;
40 
41 namespace Tgstation.Server.Host.Core
42 {
46 #pragma warning disable CA1506
48  {
52  readonly IWebHostEnvironment hostingEnvironment;
53 
58 
64  public Application(
65  IConfiguration configuration,
66  IWebHostEnvironment hostingEnvironment)
67  : base(configuration)
68  {
69  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
70  }
71 
76  public static IServerFactory CreateDefaultServerFactory()
77  => new ServerFactory(
79  IOManager);
80 
86  public void ConfigureServices(IServiceCollection services, IPostSetupServices postSetupServices)
87  {
88  ConfigureServices(services);
89 
90  if (postSetupServices == null)
91  throw new ArgumentNullException(nameof(postSetupServices));
92 
93  // configure configuration
94  services.UseStandardConfig<UpdatesConfiguration>(Configuration);
95  services.UseStandardConfig<ControlPanelConfiguration>(Configuration);
96 
97  // enable options which give us config reloading
98  services.AddOptions();
99 
100  static LogEventLevel? ConvertSeriLogLevel(LogLevel logLevel) =>
101  logLevel switch
102  {
103  LogLevel.Critical => LogEventLevel.Fatal,
104  LogLevel.Debug => LogEventLevel.Debug,
105  LogLevel.Error => LogEventLevel.Error,
106  LogLevel.Information => LogEventLevel.Information,
107  LogLevel.Trace => LogEventLevel.Verbose,
108  LogLevel.Warning => LogEventLevel.Warning,
109  LogLevel.None => null,
110  _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel)),
111  };
112 
113  var microsoftEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.MicrosoftLogLevel);
114  services.SetupLogging(
115  config =>
116  {
117  if (microsoftEventLevel.HasValue)
118  config.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
119  },
120  sinkConfig =>
121  {
122  if (postSetupServices.FileLoggingConfiguration.Disable)
123  return;
124 
125  var logPath = postSetupServices.FileLoggingConfiguration.GetFullLogDirectory(
126  IOManager,
128  postSetupServices.PlatformIdentifier);
129 
130  var logEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.LogLevel);
131 
132  var formatter = new MessageTemplateTextFormatter(
133  "{Timestamp:o} "
135  + ": [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}",
136  null);
137 
138  logPath = IOManager.ConcatPath(logPath, "tgs-.log");
139  var rollingFileConfig = sinkConfig.File(
140  formatter,
141  logPath,
142  logEventLevel ?? LogEventLevel.Verbose,
143  50 * 1024 * 1024, // 50MB max size
144  flushToDiskInterval: TimeSpan.FromSeconds(2),
145  rollingInterval: RollingInterval.Day,
146  rollOnFileSizeLimit: true);
147  });
148 
149  // configure bearer token validation
150  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
151  {
152  // this line isn't actually run until the first request is made
153  // at that point tokenFactory will be populated
154  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
155  jwtBearerOptions.Events = new JwtBearerEvents
156  {
157  // Application is our composition root so this monstrosity of a line is okay
158  // At least, that's what I tell myself to sleep at night
159  OnTokenValidated = ctx => ctx
160  .HttpContext
161  .RequestServices
162  .GetRequiredService<IClaimsInjector>()
163  .InjectClaimsIntoContext(
164  ctx,
165  ctx.HttpContext.RequestAborted)
166  };
167  });
168 
169  // WARNING: STATIC CODE
170  // fucking prevents converting 'sub' to M$ bs
171  // can't be done in the above lambda, that's too late
172  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
173 
174  // add mvc, configure the json serializer settings
175  services
176  .AddMvc(options =>
177  {
178  options.EnableEndpointRouting = false;
179  options.ReturnHttpNotAcceptable = true;
180  options.RespectBrowserAcceptHeader = true;
181  })
182  .AddNewtonsoftJson(options =>
183  {
184  options.AllowInputFormatterExceptionMessages = true;
185  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
186  options.SerializerSettings.CheckAdditionalContent = true;
187  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
188  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
189  options.SerializerSettings.Converters = new[] { new VersionConverter() };
190  });
191 
192  if (hostingEnvironment.IsDevelopment())
193  {
194  static string GetDocumentationFilePath(string assemblyLocation) => IOManager.ConcatPath(IOManager.GetDirectoryName(assemblyLocation), String.Concat(IOManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
195  var assemblyDocumentationPath = GetDocumentationFilePath(typeof(Application).Assembly.Location);
196  var apiDocumentationPath = GetDocumentationFilePath(typeof(ApiHeaders).Assembly.Location);
197  services.AddSwaggerGen(genOptions => SwaggerConfiguration.Configure(genOptions, assemblyDocumentationPath, apiDocumentationPath));
198  services.AddSwaggerGenNewtonsoftSupport();
199  }
200 
201  // enable browser detection
202  services.AddDetectionCore().AddBrowser();
203 
204  // CORS conditionally enabled later
205  services.AddCors();
206 
207  void AddTypedContext<TContext>() where TContext : DatabaseContext
208  {
209  var configureAction = DatabaseContext.GetConfigureAction<TContext>();
210 
211  services.AddDbContextPool<TContext>((serviceProvider, builder) =>
212  {
213  if (hostingEnvironment.IsDevelopment())
214  builder.EnableSensitiveDataLogging();
215 
216  var databaseConfigOptions = serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>();
217  var databaseConfig = databaseConfigOptions.Value ?? throw new InvalidOperationException("DatabaseConfiguration missing!");
218  configureAction(builder, databaseConfig);
219  });
220  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
221  }
222 
223  // add the correct database context type
224  var dbType = postSetupServices.DatabaseConfiguration.DatabaseType;
225  switch (dbType)
226  {
227  case DatabaseType.MySql:
228  case DatabaseType.MariaDB:
229  AddTypedContext<MySqlDatabaseContext>();
230  break;
231  case DatabaseType.SqlServer:
232  AddTypedContext<SqlServerDatabaseContext>();
233  break;
234  case DatabaseType.Sqlite:
235  AddTypedContext<SqliteDatabaseContext>();
236  break;
237  case DatabaseType.PostgresSql:
238  AddTypedContext<PostgresSqlDatabaseContext>();
239  break;
240  default:
241  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
242  }
243 
244  // configure other database services
245  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
246  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
247 
248  // configure security services
250  services.AddScoped<IClaimsInjector, ClaimsInjector>();
251  services.AddSingleton<IIdentityCache, IdentityCache>();
252  services.AddSingleton<ICryptographySuite, CryptographySuite>();
253  services.AddSingleton<ITokenFactory, TokenFactory>();
254  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
255 
256  // configure platform specific services
257  if (postSetupServices.PlatformIdentifier.IsWindows)
258  {
259  AddWatchdog<WindowsWatchdogFactory>(services, postSetupServices);
260  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
261  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
262  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
263  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
264  services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
265 
266  services.AddSingleton<WindowsNetworkPromptReaper>();
267  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
268  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
269  }
270  else
271  {
272  AddWatchdog<PosixWatchdogFactory>(services, postSetupServices);
273  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
274  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
275  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
276  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
277 
278  services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
279 
280  // PosixProcessFeatures also needs a IProcessExecutor for gcore
281  services.AddSingleton(x => new Lazy<IProcessExecutor>(() => x.GetRequiredService<IProcessExecutor>(), true));
282  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
283  }
284 
285  // configure misc services
286  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
287  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
288  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
289  services.AddSingleton<IServerPortProvider, ServerPortProivder>();
290  services.AddSingleton<ITopicClientFactory, TopicClientFactory>();
291 
292  // configure component services
293  services.AddSingleton<ILibGit2RepositoryFactory, LibGit2RepositoryFactory>();
294  services.AddSingleton<ILibGit2Commands, LibGit2Commands>();
295  services.AddSingleton<IProviderFactory, ProviderFactory>();
296  services.AddSingleton<IChatManagerFactory, ChatManagerFactory>();
297  services.AddSingleton<IInstanceFactory, InstanceFactory>();
298 
299  // configure root services
300  services.AddSingleton<IJobManager, JobManager>();
301 
302  services.AddSingleton<InstanceManager>();
303  services.AddSingleton<IBridgeDispatcher>(x => x.GetRequiredService<InstanceManager>());
304  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
305  }
306 
313  static void AddWatchdog<TSystemWatchdogFactory>(IServiceCollection services, IPostSetupServices postSetupServices)
314  where TSystemWatchdogFactory : class, IWatchdogFactory
315  {
316  if (postSetupServices.GeneralConfiguration.UseBasicWatchdog)
317  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
318  else
319  services.AddSingleton<IWatchdogFactory, TSystemWatchdogFactory>();
320  }
321 
323  protected override void ConfigureHostedService(IServiceCollection services)
324  => services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
325 
336  public void Configure(
337  IApplicationBuilder applicationBuilder,
338  IServerControl serverControl,
339  ITokenFactory tokenFactory,
340  IInstanceManager instanceManager,
341  IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
342  IOptions<GeneralConfiguration> generalConfigurationOptions,
343  ILogger<Application> logger)
344  {
345  if (applicationBuilder == null)
346  throw new ArgumentNullException(nameof(applicationBuilder));
347  if (serverControl == null)
348  throw new ArgumentNullException(nameof(serverControl));
349 
350  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
351 
352  var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
353  var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
354 
355  if (logger == null)
356  throw new ArgumentNullException(nameof(logger));
357 
358  logger.LogDebug("Content Root: {0}", hostingEnvironment.ContentRootPath);
359  logger.LogTrace("Web Root: {0}", hostingEnvironment.WebRootPath);
360 
361  if (generalConfiguration.MinimumPasswordLength > Limits.MaximumStringLength)
362  {
363  logger.LogCritical("Configured minimum password length ({0}) is greater than the maximum database string length ({1})!");
364  serverControl.Die(new InvalidOperationException("Minimum password length greater than database limit!"));
365  return;
366  }
367 
368  // attempt to restart the server if the configuration changes
369  if (serverControl.WatchdogPresent)
370  ChangeToken.OnChange(Configuration.GetReloadToken, () =>
371  {
372  logger.LogInformation("Configuration change detected");
373  serverControl.Restart();
374  });
375 
376  // setup the HTTP request pipeline
377  // Final point where we wrap exceptions in a 500 (ErrorMessage) response
378  applicationBuilder.UseServerErrorHandling();
379 
380  // 503 requests made while the application is starting
381  applicationBuilder.UseAsyncInitialization(async (cancellationToken) =>
382  {
383  var tcs = new TaskCompletionSource<object>();
384  using (cancellationToken.Register(() => tcs.SetCanceled()))
385  await Task.WhenAny(tcs.Task, instanceManager.Ready).ConfigureAwait(false);
386  });
387 
388  // suppress OperationCancelledExceptions, they are just aborted HTTP requests
389  applicationBuilder.UseCancelledRequestSuppression();
390 
391  if (hostingEnvironment.IsDevelopment())
392  {
393  applicationBuilder.UseSwagger();
394  applicationBuilder.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TGS API V4"));
395  logger.LogTrace("Swagger API generation enabled");
396  }
397 
398  // Set up CORS based on configuration if necessary
399  Action<CorsPolicyBuilder> corsBuilder = null;
400  if (controlPanelConfiguration.AllowAnyOrigin)
401  {
402  logger.LogTrace("Access-Control-Allow-Origin: *");
403  corsBuilder = builder => builder.AllowAnyOrigin();
404  }
405  else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
406  {
407  logger.LogTrace("Access-Control-Allow-Origin: ", String.Join(',', controlPanelConfiguration.AllowedOrigins));
408  corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
409  }
410 
411  var originalBuilder = corsBuilder;
412  corsBuilder = builder =>
413  {
414  builder.AllowAnyHeader().AllowAnyMethod();
415  originalBuilder?.Invoke(builder);
416  };
417  applicationBuilder.UseCors(corsBuilder);
418 
419  // spa loading if necessary
420  if (controlPanelConfiguration.Enable)
421  {
422  logger.LogWarning("Web control panel enabled. This is a highly WIP feature!");
423  applicationBuilder.UseStaticFiles();
424  }
425  else
426  logger.LogDebug("Web control panel disabled!");
427 
428  logger.LogDebug("Starting hosting...");
429 
430  // authenticate JWT tokens using our security pipeline if present, returns 401 if bad
431  applicationBuilder.UseAuthentication();
432 
433  // suppress and log database exceptions
434  applicationBuilder.UseDbConflictHandling();
435 
436  // majority of handling is done in the controllers
437  applicationBuilder.UseMvc();
438 
439  // 404 anything that gets this far
440  }
441  }
442 }
LogLevel MicrosoftLogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs for Microsoft library sources ...
const int MaximumStringLength
Length limit for strings in fields.
Definition: Limits.cs:11
IPostWriteHandler for POSIX systems
Handles changing file modes/permissions after writing
void ConfigureServices(IServiceCollection services, IPostSetupServices postSetupServices)
Configure the Application&#39;s services.
Definition: Application.cs:86
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
For low level interactions with a LibGit2Sharp.IRepository.
bool WatchdogPresent
if live updates are supported, . ApplyUpdate(Version, Uri, IIOManager) and Restart will fail if this ...
Task Die(Exception exception)
Kill the server with a fatal exception
LogLevel LogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs
const string SerilogContextTemplate
Common template used for adding our custom log context to serilog.
For initially setting up a database.
ITokenFactory tokenFactory
The ITokenFactory for the Application
Definition: Application.cs:57
IPlatformIdentifier PlatformIdentifier
The IPlatformIdentifier.
Sanity limits to prevent users from overloading
Definition: Limits.cs:6
Sets up dependency injection.
Definition: Application.cs:47
Implements various filters for Swashbuckle.
Implementation of IServerFactory.
ISystemIdentityFactory for windows systems. Uses long running tasks due to potential networked domain...
Task Ready
Task that completes when the IInstanceManager finishes initializing.
Contains various cryptographic functions
Backend abstract implementation of IDatabaseContext
Application(IConfiguration configuration, IWebHostEnvironment hostingEnvironment)
Construct an Application
Definition: Application.cs:64
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:17
readonly IWebHostEnvironment hostingEnvironment
The IWebHostEnvironment for the Application.
Definition: Application.cs:52
DatabaseConfiguration DatabaseConfiguration
The Configuration.DatabaseConfiguration.
Configuration for the automatic update system
For downloading and installing BYOND extractions for a given system
void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IInstanceManager instanceManager, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< Application > logger)
Configure the Application
Definition: Application.cs:336
For accessing the disk in a synchronous manner
DatabaseType DatabaseType
The Configuration.DatabaseType to create
string GetFullLogDirectory(IIOManager ioManager, IAssemblyInformationProvider assemblyInformationProvider, IPlatformIdentifier platformIdentifier)
Gets the evaluated log Directory.
IPostWriteHandler for Windows systems
DI root for configuring a SetupWizard.
Set of objects needed to configure an Core.Application.
JsonConverter for serializing Versions for BYOND.
static void Configure(SwaggerGenOptions swaggerGenOptions, string assemblyDocumentationPath, string apiDocumentationPath)
Configure the swagger settings.
TokenValidationParameters ValidationParameters
The TokenValidationParameters for the ITokenFactory
Manages the runtime of Jobs
Definition: IJobManager.cs:13
On Windows, DreamDaemon will show an unskippable prompt when using /world/proc/OpenPort(). This looks out for those prompts and immediately clicks "Yes" if the owning process has registered for it
For injecting global::System.Security.Claims.Claims that Controllers.TgsAuthorizeAttribute can look f...
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
FileLoggingConfiguration FileLoggingConfiguration
The Configuration.FileLoggingConfiguration.
Abstraction for suspending and resuming processes.
Provides access to the server&#39;s HttpApiPort.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
bool IsWindows
If the current platform is a Windows platform