tgstation-server  4.3.2
The /tg/station 13 server suite
Application.cs
Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Cyberboss.AspNetCore.AsyncInitializer;
3 using Microsoft.AspNetCore.Authentication.JwtBearer;
4 using Microsoft.AspNetCore.Builder;
5 using Microsoft.AspNetCore.Cors.Infrastructure;
6 using Microsoft.AspNetCore.Hosting;
7 using Microsoft.AspNetCore.Identity;
8 using Microsoft.Extensions.Configuration;
9 using Microsoft.Extensions.DependencyInjection;
10 using Microsoft.Extensions.Hosting;
11 using Microsoft.Extensions.Logging;
12 using Microsoft.Extensions.Options;
13 using Microsoft.Extensions.Primitives;
14 using Newtonsoft.Json;
15 using Serilog;
16 using Serilog.Events;
17 using Serilog.Formatting.Display;
18 using System;
19 using System.Globalization;
20 using System.IdentityModel.Tokens.Jwt;
21 using System.Threading.Tasks;
22 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  // common app data is C:/ProgramData on windows, else /usr/share
126  var logPath = !String.IsNullOrEmpty(postSetupServices.FileLoggingConfiguration.Directory)
127  ? postSetupServices.FileLoggingConfiguration.Directory
128  : IOManager.ConcatPath(
129  Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
131  "Logs");
132 
133  var logEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.LogLevel);
134 
135  var formatter = new MessageTemplateTextFormatter(
136  "{Timestamp:o} "
138  + ": [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}",
139  null);
140 
141  logPath = IOManager.ConcatPath(logPath, "tgs-.log");
142  var rollingFileConfig = sinkConfig.File(
143  formatter,
144  logPath,
145  logEventLevel ?? LogEventLevel.Verbose,
146  50 * 1024 * 1024, // 50MB max size
147  flushToDiskInterval: TimeSpan.FromSeconds(2),
148  rollingInterval: RollingInterval.Day,
149  rollOnFileSizeLimit: true);
150  });
151 
152  // configure bearer token validation
153  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
154  {
155  // this line isn't actually run until the first request is made
156  // at that point tokenFactory will be populated
157  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
158  jwtBearerOptions.Events = new JwtBearerEvents
159  {
160  // Application is our composition root so this monstrosity of a line is okay
161  // At least, that's what I tell myself to sleep at night
162  OnTokenValidated = ctx => ctx
163  .HttpContext
164  .RequestServices
165  .GetRequiredService<IClaimsInjector>()
166  .InjectClaimsIntoContext(
167  ctx,
168  ctx.HttpContext.RequestAborted)
169  };
170  });
171 
172  // WARNING: STATIC CODE
173  // fucking prevents converting 'sub' to M$ bs
174  // can't be done in the above lambda, that's too late
175  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
176 
177  // add mvc, configure the json serializer settings
178  services
179  .AddMvc(options =>
180  {
181  options.EnableEndpointRouting = false;
182  })
183  .AddNewtonsoftJson(options =>
184  {
185  options.AllowInputFormatterExceptionMessages = true;
186  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
187  options.SerializerSettings.CheckAdditionalContent = true;
188  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
189  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
190  options.SerializerSettings.Converters = new[] { new VersionConverter() };
191  });
192 
193  if (hostingEnvironment.IsDevelopment())
194  {
195  static string GetDocumentationFilePath(string assemblyLocation) => IOManager.ConcatPath(IOManager.GetDirectoryName(assemblyLocation), String.Concat(IOManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
196  var assemblyDocumentationPath = GetDocumentationFilePath(typeof(Application).Assembly.Location);
197  var apiDocumentationPath = GetDocumentationFilePath(typeof(ApiHeaders).Assembly.Location);
198  services.AddSwaggerGen(genOptions => SwaggerConfiguration.Configure(genOptions, assemblyDocumentationPath, apiDocumentationPath));
199  services.AddSwaggerGenNewtonsoftSupport();
200  }
201 
202  // enable browser detection
203  services.AddDetectionCore().AddBrowser();
204 
205  // CORS conditionally enabled later
206  services.AddCors();
207 
208  void AddTypedContext<TContext>() where TContext : DatabaseContext
209  {
210  services.AddDbContext<TContext>(builder =>
211  {
212  if (hostingEnvironment.IsDevelopment())
213  builder.EnableSensitiveDataLogging();
214  });
215  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
216  }
217 
218  // add the correct database context type
219  var dbType = postSetupServices.DatabaseConfiguration.DatabaseType;
220  switch (dbType)
221  {
222  case DatabaseType.MySql:
223  case DatabaseType.MariaDB:
224  AddTypedContext<MySqlDatabaseContext>();
225  break;
226  case DatabaseType.SqlServer:
227  AddTypedContext<SqlServerDatabaseContext>();
228  break;
229  case DatabaseType.Sqlite:
230  AddTypedContext<SqliteDatabaseContext>();
231  break;
232  case DatabaseType.PostgresSql:
233  AddTypedContext<PostgresSqlDatabaseContext>();
234  break;
235  default:
236  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
237  }
238 
239  // configure other database services
240  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
241  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
242 
243  // configure security services
245  services.AddScoped<IClaimsInjector, ClaimsInjector>();
246  services.AddSingleton<IIdentityCache, IdentityCache>();
247  services.AddSingleton<ICryptographySuite, CryptographySuite>();
248  services.AddSingleton<ITokenFactory, TokenFactory>();
249  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
250 
251  // configure platform specific services
252  if (postSetupServices.PlatformIdentifier.IsWindows)
253  {
254  if (postSetupServices.GeneralConfiguration.UseBasicWatchdogOnWindows)
255  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
256  else
257  services.AddSingleton<IWatchdogFactory, WindowsWatchdogFactory>();
258 
259  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
260  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
261  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
262  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
263  services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
264 
265  services.AddSingleton<WindowsNetworkPromptReaper>();
266  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
267  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
268  }
269  else
270  {
271  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
272  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
273  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
274  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
275  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
276  services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
277  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
278  }
279 
280  // configure misc services
281  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
282  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
283  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
284  services.AddSingleton<IServerPortProvider, ServerPortProivder>();
285  services.AddSingleton<ITopicClient, TopicClient>();
286  services.AddSingleton(new SocketParameters
287  {
288  ReceiveTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
289  SendTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
290  ConnectTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
291  DisconnectTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout)
292  });
293 
294  // configure component services
295  services.AddSingleton<ILibGit2RepositoryFactory, LibGit2RepositoryFactory>();
296  services.AddSingleton<ILibGit2Commands, LibGit2Commands>();
297  services.AddSingleton<IProviderFactory, ProviderFactory>();
298  services.AddSingleton<IChatManagerFactory, ChatManagerFactory>();
299  services.AddSingleton<IInstanceFactory, InstanceFactory>();
300 
301  // configure root services
302  services.AddSingleton<IJobManager, JobManager>();
303 
304  services.AddSingleton<InstanceManager>();
305  services.AddSingleton<IBridgeDispatcher>(x => x.GetRequiredService<InstanceManager>());
306  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
307  }
308 
310  protected override void ConfigureHostedService(IServiceCollection services)
311  => services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
312 
323  public void Configure(
324  IApplicationBuilder applicationBuilder,
325  IServerControl serverControl,
326  ITokenFactory tokenFactory,
327  IInstanceManager instanceManager,
328  IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
329  IOptions<GeneralConfiguration> generalConfigurationOptions,
330  ILogger<Application> logger)
331  {
332  if (applicationBuilder == null)
333  throw new ArgumentNullException(nameof(applicationBuilder));
334  if (serverControl == null)
335  throw new ArgumentNullException(nameof(serverControl));
336 
337  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
338 
339  var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
340  var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
341 
342  if (logger == null)
343  throw new ArgumentNullException(nameof(logger));
344 
345  logger.LogDebug("Content Root: {0}", hostingEnvironment.ContentRootPath);
346  logger.LogTrace("Web Root: {0}", hostingEnvironment.WebRootPath);
347 
348  if (generalConfiguration.MinimumPasswordLength > Limits.MaximumStringLength)
349  {
350  logger.LogCritical("Configured minimum password length ({0}) is greater than the maximum database string length ({1})!");
351  serverControl.Die(new InvalidOperationException("Minimum password length greater than database limit!"));
352  return;
353  }
354 
355  // attempt to restart the server if the configuration changes
356  if (serverControl.WatchdogPresent)
357  ChangeToken.OnChange(Configuration.GetReloadToken, () => serverControl.Restart());
358 
359  // setup the HTTP request pipeline
360  // Final point where we wrap exceptions in a 500 (ErrorMessage) response
361  applicationBuilder.UseServerErrorHandling();
362 
363  // 503 requests made while the application is starting
364  applicationBuilder.UseAsyncInitialization(async (cancellationToken) =>
365  {
366  var tcs = new TaskCompletionSource<object>();
367  using (cancellationToken.Register(() => tcs.SetCanceled()))
368  await Task.WhenAny(tcs.Task, instanceManager.Ready).ConfigureAwait(false);
369  });
370 
371  // suppress OperationCancelledExceptions, they are just aborted HTTP requests
372  applicationBuilder.UseCancelledRequestSuppression();
373 
374  if (hostingEnvironment.IsDevelopment())
375  {
376  applicationBuilder.UseSwagger();
377  applicationBuilder.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TGS API V4"));
378  logger.LogTrace("Swagger API generation enabled");
379  }
380 
381  // Set up CORS based on configuration if necessary
382  Action<CorsPolicyBuilder> corsBuilder = null;
383  if (controlPanelConfiguration.AllowAnyOrigin)
384  {
385  logger.LogTrace("Access-Control-Allow-Origin: *");
386  corsBuilder = builder => builder.AllowAnyOrigin();
387  }
388  else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
389  {
390  logger.LogTrace("Access-Control-Allow-Origin: ", String.Join(',', controlPanelConfiguration.AllowedOrigins));
391  corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
392  }
393 
394  var originalBuilder = corsBuilder;
395  corsBuilder = builder =>
396  {
397  builder.AllowAnyHeader().AllowAnyMethod();
398  originalBuilder?.Invoke(builder);
399  };
400  applicationBuilder.UseCors(corsBuilder);
401 
402  // spa loading if necessary
403  if (controlPanelConfiguration.Enable)
404  {
405  logger.LogWarning("Web control panel enabled. This is a highly WIP feature!");
406  applicationBuilder.UseStaticFiles();
407  }
408  else
409  logger.LogDebug("Web control panel disabled!");
410 
411  // authenticate JWT tokens using our security pipeline if present, returns 401 if bad
412  applicationBuilder.UseAuthentication();
413 
414  // suppress and log database exceptions
415  applicationBuilder.UseDbConflictHandling();
416 
417  // majority of handling is done in the controllers
418  applicationBuilder.UseMvc();
419 
420  // 404 anything that gets this far
421  }
422  }
423 }
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 seeding 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:323
For accessing the disk in a synchronous manner
DatabaseType DatabaseType
The Configuration.DatabaseType to create
bool UseBasicWatchdogOnWindows
If the Components.Watchdog.WindowsWatchdog should not be used if it is available. ...
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.
GeneralConfiguration GeneralConfiguration
The Configuration.GeneralConfiguration.
TokenValidationParameters ValidationParameters
The TokenValidationParameters for the ITokenFactory
Manages the runtime of Jobs
Definition: IJobManager.cs:13
int ByondTopicTimeout
The timeout in milliseconds for sending and receiving topics to/from DreamDaemon. Note that a single ...
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...
Represents a BYOND installation
Definition: Byond.cs:8
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