tgstation-server 6.19.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
Application.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Frozen;
3using System.Collections.Generic;
4using System.Globalization;
5using System.IO.Abstractions;
6using System.Threading.Tasks;
7using System.Web;
8
9using Cyberboss.AspNetCore.AsyncInitializer;
10
11using Elastic.CommonSchema.Serilog;
12
13using HotChocolate.AspNetCore;
14
15using Microsoft.AspNetCore.Authentication;
16using Microsoft.AspNetCore.Authentication.Cookies;
17using Microsoft.AspNetCore.Authentication.JwtBearer;
18using Microsoft.AspNetCore.Authentication.OpenIdConnect;
19using Microsoft.AspNetCore.Authorization;
20using Microsoft.AspNetCore.Builder;
21using Microsoft.AspNetCore.Cors.Infrastructure;
22using Microsoft.AspNetCore.Hosting;
23using Microsoft.AspNetCore.Http;
24using Microsoft.AspNetCore.Http.Connections;
25using Microsoft.AspNetCore.HttpOverrides;
26using Microsoft.AspNetCore.Identity;
27using Microsoft.AspNetCore.Mvc.Infrastructure;
28using Microsoft.AspNetCore.SignalR;
29using Microsoft.Extensions.Configuration;
30using Microsoft.Extensions.DependencyInjection;
31using Microsoft.Extensions.Hosting;
32using Microsoft.Extensions.Logging;
33using Microsoft.Extensions.Options;
34using Microsoft.IdentityModel.Protocols.OpenIdConnect;
35
36using Newtonsoft.Json;
37
38using Prometheus;
39
40using Serilog;
41using Serilog.Events;
42using Serilog.Formatting.Display;
43using Serilog.Sinks.Elasticsearch;
44
77
79{
83#pragma warning disable CA1506
84 public sealed class Application : SetupApplication
85 {
89 readonly IWebHostEnvironment hostingEnvironment;
90
94 ITokenFactory? tokenFactory;
95
100 public static IServerFactory CreateDefaultServerFactory()
101 {
102 var assemblyInformationProvider = new AssemblyInformationProvider();
103 var fileSystem = new FileSystem();
104 var ioManager = new DefaultIOManager(fileSystem);
105 return new ServerFactory(
106 assemblyInformationProvider,
107 ioManager,
108 fileSystem);
109 }
110
117 static void AddWatchdog<TSystemWatchdogFactory>(IServiceCollection services, IPostSetupServices postSetupServices)
118 where TSystemWatchdogFactory : class, IWatchdogFactory
119 {
120 if (postSetupServices.GeneralConfiguration.UseBasicWatchdog)
121 services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
122 else
123 services.AddSingleton<IWatchdogFactory, TSystemWatchdogFactory>();
124 }
125
131 static string GetOidcScheme(string schemeKey)
132 => AuthenticationContextFactory.OpenIDConnectAuthenticationSchemePrefix + schemeKey;
133
139 public Application(
140 IConfiguration configuration,
141 IWebHostEnvironment hostingEnvironment)
142 : base(configuration)
143 {
144 this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
145 }
146
155 public void ConfigureServices(
156 IServiceCollection services,
157 IAssemblyInformationProvider assemblyInformationProvider,
158 IIOManager ioManager,
159 IPostSetupServices postSetupServices,
160 IFileSystem fileSystem)
161 {
162 ConfigureServices(services, assemblyInformationProvider, ioManager);
163
164 ArgumentNullException.ThrowIfNull(postSetupServices);
165
166 // configure configuration
167 services.UseStandardConfig<UpdatesConfiguration>(Configuration);
168 services.UseStandardConfig<ControlPanelConfiguration>(Configuration);
169 services.UseStandardConfig<SwarmConfiguration>(Configuration);
170 services.UseStandardConfig<SessionConfiguration>(Configuration);
171
172 // enable options which give us config reloading
173 services.AddOptions();
174
175 // Set the timeout for IHostedService.StopAsync
176 services.Configure<HostOptions>(
177 opts => opts.ShutdownTimeout = TimeSpan.FromMinutes(postSetupServices.GeneralConfiguration.RestartTimeoutMinutes));
178
179 static LogEventLevel? ConvertSeriLogLevel(LogLevel logLevel) =>
180 logLevel switch
181 {
182 LogLevel.Critical => LogEventLevel.Fatal,
183 LogLevel.Debug => LogEventLevel.Debug,
184 LogLevel.Error => LogEventLevel.Error,
185 LogLevel.Information => LogEventLevel.Information,
186 LogLevel.Trace => LogEventLevel.Verbose,
187 LogLevel.Warning => LogEventLevel.Warning,
188 LogLevel.None => null,
189 _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel)),
190 };
191
192 var microsoftEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.MicrosoftLogLevel);
193 var elasticsearchConfiguration = postSetupServices.ElasticsearchConfiguration;
194 services.SetupLogging(
195 config =>
196 {
197 if (microsoftEventLevel.HasValue)
198 {
199 config.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
200 config.MinimumLevel.Override("System.Net.Http.HttpClient", microsoftEventLevel.Value);
201 }
202 },
203 sinkConfig =>
204 {
205 if (postSetupServices.FileLoggingConfiguration.Disable)
206 return;
207
208 var logPath = postSetupServices.FileLoggingConfiguration.GetFullLogDirectory(
209 ioManager,
210 assemblyInformationProvider,
211 postSetupServices.PlatformIdentifier);
212
213 var logEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.LogLevel);
214
215 var formatter = new MessageTemplateTextFormatter(
216 "{Timestamp:o} "
218 + "): [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}",
219 null);
220
221 logPath = ioManager.ConcatPath(logPath, "tgs-.log");
222 var rollingFileConfig = sinkConfig.File(
223 formatter,
224 logPath,
225 logEventLevel ?? LogEventLevel.Verbose,
226 50 * 1024 * 1024, // 50MB max size
227 flushToDiskInterval: TimeSpan.FromSeconds(2),
228 rollingInterval: RollingInterval.Day,
229 rollOnFileSizeLimit: true);
230 },
231 elasticsearchConfiguration.Enable
232 ? new ElasticsearchSinkOptions(elasticsearchConfiguration.Host ?? throw new InvalidOperationException($"Missing {ElasticsearchConfiguration.Section}:{nameof(elasticsearchConfiguration.Host)}!"))
233 {
234 // Yes I know this means they cannot use a self signed cert unless they also have authentication, but lets be real here
235 // No one is going to be doing one of those but not the other
236 ModifyConnectionSettings = connectionConfigration => (!String.IsNullOrWhiteSpace(elasticsearchConfiguration.Username) && !String.IsNullOrWhiteSpace(elasticsearchConfiguration.Password))
237 ? connectionConfigration
238 .BasicAuthentication(
239 elasticsearchConfiguration.Username,
240 elasticsearchConfiguration.Password)
241 .ServerCertificateValidationCallback((o, certificate, chain, errors) => true)
242 : null,
243 CustomFormatter = new EcsTextFormatter(),
244 AutoRegisterTemplate = true,
245 AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7,
246 IndexFormat = "tgs-logs",
247 }
248 : null,
249 postSetupServices.InternalConfiguration,
250 postSetupServices.FileLoggingConfiguration);
251
252 // configure authentication pipeline
253 ConfigureAuthenticationPipeline(services, postSetupServices.SecurityConfiguration);
254
255 // add mvc, configure the json serializer settings
256 var jsonVersionConverterList = new List<JsonConverter>
257 {
258 new VersionConverter(),
259 };
260
261 void ConfigureNewtonsoftJsonSerializerSettingsForApi(JsonSerializerSettings settings)
262 {
263 settings.NullValueHandling = NullValueHandling.Ignore;
264 settings.CheckAdditionalContent = true;
265 settings.MissingMemberHandling = MissingMemberHandling.Error;
266 settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
267 settings.Converters = jsonVersionConverterList;
268 }
269
270 services
271 .AddMvc(options =>
272 {
273 options.ReturnHttpNotAcceptable = true;
274 options.RespectBrowserAcceptHeader = true;
275 })
276 .AddNewtonsoftJson(options =>
277 {
278 options.AllowInputFormatterExceptionMessages = true;
279 ConfigureNewtonsoftJsonSerializerSettingsForApi(options.SerializerSettings);
280 });
281
282 services.AddSignalR(
283 options =>
284 {
285 options.AddFilter<AuthorizationContextHubFilter>();
286 })
287 .AddNewtonsoftJsonProtocol(options =>
288 {
289 ConfigureNewtonsoftJsonSerializerSettingsForApi(options.PayloadSerializerSettings);
290 });
291
292 services.AddHub<JobsHub, IJobsHub>();
293
294 if (postSetupServices.GeneralConfiguration.HostApiDocumentation)
295 {
296 string GetDocumentationFilePath(string assemblyLocation) => ioManager.ConcatPath(ioManager.GetDirectoryName(assemblyLocation), String.Concat(ioManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
297 var assemblyDocumentationPath = GetDocumentationFilePath(GetType().Assembly.Location);
298 var apiDocumentationPath = GetDocumentationFilePath(typeof(ApiHeaders).Assembly.Location);
299 services.AddSwaggerGen(genOptions => SwaggerConfiguration.Configure(genOptions, assemblyDocumentationPath, apiDocumentationPath));
300 services.AddSwaggerGenNewtonsoftSupport();
301 }
302
303 // CORS conditionally enabled later
304 services.AddCors();
305
306 // Enable managed HTTP clients
307 services
308 .AddHttpClient()
309 .ConfigureHttpClientDefaults(
310 builder => builder.ConfigureHttpClient(
311 client => client.DefaultRequestHeaders.UserAgent.Add(
312 assemblyInformationProvider.ProductInfoHeaderValue)));
313
314 // configure metrics
315 var prometheusPort = postSetupServices.GeneralConfiguration.PrometheusPort;
316
317 services.AddSingleton<IMetricFactory>(_ => Metrics.DefaultFactory);
318 services.AddSingleton<ICollectorRegistry>(_ => Metrics.DefaultRegistry);
319
320 if (prometheusPort.HasValue && prometheusPort != postSetupServices.GeneralConfiguration.ApiPort)
321 services.AddMetricServer(options => options.Port = prometheusPort.Value);
322
323 services.UseHttpClientMetrics();
324
325 var healthChecksBuilder = services
326 .AddHealthChecks()
327 .ForwardToPrometheus();
328
329 // configure graphql
330 services
332 .AddGraphQLServer()
333 .ConfigureGraphQLServer();
334
335 void AddTypedContext<TContext>()
336 where TContext : DatabaseContext
337 {
338 var configureAction = DatabaseContext.GetConfigureAction<TContext>();
339
340 services.AddDbContextPool<TContext>((serviceProvider, builder) =>
341 {
342 if (hostingEnvironment.IsDevelopment())
343 builder.EnableSensitiveDataLogging();
344
345 var databaseConfigOptions = serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>();
346 var databaseConfig = databaseConfigOptions.Value ?? throw new InvalidOperationException("DatabaseConfiguration missing!");
347 configureAction(builder, databaseConfig);
348 });
349 services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
350
351 healthChecksBuilder
352 .AddDbContextCheck<TContext>();
353 }
354
355 // add the correct database context type
356 var dbType = postSetupServices.DatabaseConfiguration.DatabaseType;
357 switch (dbType)
358 {
359 case DatabaseType.MySql:
360 case DatabaseType.MariaDB:
361 AddTypedContext<MySqlDatabaseContext>();
362 break;
363 case DatabaseType.SqlServer:
364 AddTypedContext<SqlServerDatabaseContext>();
365 break;
366 case DatabaseType.Sqlite:
367 AddTypedContext<SqliteDatabaseContext>();
368 break;
369 case DatabaseType.PostgresSql:
370 AddTypedContext<PostgresSqlDatabaseContext>();
371 break;
372 default:
373 throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
374 }
375
376 // configure other database services
377 services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
378 services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
379
380 // configure other security services
381 services.AddScoped<IOAuthProviders, OAuthProviders>();
382 services.AddSingleton<IIdentityCache, IdentityCache>();
383 services.AddSingleton<ICryptographySuite, CryptographySuite>();
384 services.AddSingleton<ITokenFactory, TokenFactory>();
386 services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
387
388 // configure platform specific services
389 if (postSetupServices.PlatformIdentifier.IsWindows)
390 {
391 AddWatchdog<WindowsWatchdogFactory>(services, postSetupServices);
394 services.AddSingleton<ByondInstallerBase, WindowsByondInstaller>();
395 services.AddSingleton<OpenDreamInstaller, WindowsOpenDreamInstaller>();
396 services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
397 services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
398
399 services.AddSingleton<WindowsNetworkPromptReaper>();
400 services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
401 services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
402 }
403 else
404 {
405 AddWatchdog<PosixWatchdogFactory>(services, postSetupServices);
406 services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
407 services.AddSingleton<IFilesystemLinkFactory, PosixFilesystemLinkFactory>();
408 services.AddSingleton<ByondInstallerBase, PosixByondInstaller>();
409 services.AddSingleton<OpenDreamInstaller>();
410 services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
411
412 services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
413 services.AddHostedService<PosixProcessFeatures>();
414
415 // PosixProcessFeatures also needs a IProcessExecutor for gcore
416 services.AddSingleton(x => new Lazy<IProcessExecutor>(() => x.GetRequiredService<IProcessExecutor>(), true));
417 services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
418
419 services.AddHostedService<PosixSignalHandler>();
420 }
421
422 // only global repo manager should be for the OD repo
423 // god help me if we need more
424 var openDreamRepositoryDirectory = ioManager.ConcatPath(
425 ioManager.GetPathInLocalDirectory(assemblyInformationProvider),
426 "OpenDreamRepository");
427 services.AddSingleton(
428 services => services
429 .GetRequiredService<IRepositoryManagerFactory>()
430 .CreateRepositoryManager(
431 services.GetRequiredService<IIOManager>().CreateResolverForSubdirectory(
432 openDreamRepositoryDirectory),
433 new NoopEventConsumer()));
434
435 services.AddSingleton(
436 serviceProvider => new Dictionary<EngineType, IEngineInstaller>
437 {
438 { EngineType.Byond, serviceProvider.GetRequiredService<ByondInstallerBase>() },
439 { EngineType.OpenDream, serviceProvider.GetRequiredService<OpenDreamInstaller>() },
440 }
441 .ToFrozenDictionary());
442 services.AddSingleton<IEngineInstaller, DelegatingEngineInstaller>();
443
444 if (postSetupServices.InternalConfiguration.UsingSystemD)
445 services.AddHostedService<SystemDManager>();
446
447 // configure file transfer services
448 services.AddSingleton<FileTransferService>();
449 services.AddSingleton<IFileTransferStreamHandler>(x => x.GetRequiredService<FileTransferService>());
450 services.AddSingleton<IFileTransferTicketProvider>(x => x.GetRequiredService<FileTransferService>());
452
453 // configure swarm service
454 services.AddSingleton<SwarmService>();
455 services.AddSingleton<ISwarmService>(x => x.GetRequiredService<SwarmService>());
456 services.AddSingleton<ISwarmOperations>(x => x.GetRequiredService<SwarmService>());
457 services.AddSingleton<ISwarmServiceController>(x => x.GetRequiredService<SwarmService>());
458
459 // configure component services
460 services.AddSingleton<IPortAllocator, PortAllocator>();
461 services.AddSingleton<IInstanceFactory, InstanceFactory>();
462 services.AddSingleton<IGitRemoteFeaturesFactory, GitRemoteFeaturesFactory>();
463 services.AddSingleton<ILibGit2RepositoryFactory, LibGit2RepositoryFactory>();
464 services.AddSingleton<ILibGit2Commands, LibGit2Commands>();
465 services.AddSingleton<IRepositoryManagerFactory, RepostoryManagerFactory>();
467 services.AddChatProviderFactory();
468 services.AddSingleton<IChatManagerFactory, ChatManagerFactory>();
469 services.AddSingleton<IServerUpdater, ServerUpdater>();
470 services.AddSingleton<IServerUpdateInitiator, ServerUpdateInitiator>();
471 services.AddSingleton<IDotnetDumpService, DotnetDumpService>();
472
473 // configure authorities
474 services.AddScoped(typeof(IRestAuthorityInvoker<>), typeof(RestAuthorityInvoker<>));
475 services.AddScoped(typeof(IGraphQLAuthorityInvoker<>), typeof(GraphQLAuthorityInvoker<>));
476 services.AddScoped<ILoginAuthority, LoginAuthority>();
477 services.AddScoped<IUserAuthority, UserAuthority>();
478 services.AddScoped<IUserGroupAuthority, UserGroupAuthority>();
479 services.AddScoped<IPermissionSetAuthority, PermissionSetAuthority>();
481 services.AddScoped<IChatAuthority, ChatAuthority>();
482
483 // configure misc services
484 services.AddSingleton<IProcessExecutor, ProcessExecutor>();
485 services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
486 services.AddSingleton<IServerPortProvider, ServerPortProivder>();
487 services.AddSingleton<ITopicClientFactory, TopicClientFactory>();
488 services.AddSingleton(fileSystem);
489 services.AddHostedService<CommandPipeManager>();
490
491 services.AddFileDownloader();
492 services.AddGitHub();
493
494 // configure root services
495 services.AddSingleton<JobService>();
496 services.AddSingleton<IJobService>(provider => provider.GetRequiredService<JobService>());
497 services.AddSingleton<IJobsHubUpdater>(provider => provider.GetRequiredService<JobService>());
498 services.AddSingleton<IJobManager>(x => x.GetRequiredService<IJobService>());
499 services.AddSingleton<JobsHubGroupMapper>();
500 services.AddSingleton<IPermissionsUpdateNotifyee>(provider => provider.GetRequiredService<JobsHubGroupMapper>());
501 services.AddSingleton<IHostedService>(x => x.GetRequiredService<JobsHubGroupMapper>()); // bit of a hack, but we need this to load immediated
502
503 services.AddSingleton<InstanceManager>();
504 services.AddSingleton<IBridgeDispatcher>(x => x.GetRequiredService<InstanceManager>());
505 services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
506 }
507
523 public void Configure(
524 IApplicationBuilder applicationBuilder,
525 IServerControl serverControl,
526 ITokenFactory tokenFactory,
527 IServerPortProvider serverPortProvider,
528 IAssemblyInformationProvider assemblyInformationProvider,
529 IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
530 IOptions<GeneralConfiguration> generalConfigurationOptions,
531 IOptions<DatabaseConfiguration> databaseConfigurationOptions,
532 IOptions<SecurityConfiguration> securityConfigurationOptions,
533 IOptions<SwarmConfiguration> swarmConfigurationOptions,
534 IOptions<InternalConfiguration> internalConfigurationOptions,
535 ILogger<Application> logger)
536 {
537 ArgumentNullException.ThrowIfNull(applicationBuilder);
538 ArgumentNullException.ThrowIfNull(serverControl);
539
540 this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
541
542 ArgumentNullException.ThrowIfNull(serverPortProvider);
543 ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
544
545 var controlPanelConfiguration = (controlPanelConfigurationOptions ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions))).Value;
546 var generalConfiguration = (generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions))).Value;
547 var databaseConfiguration = (databaseConfigurationOptions ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions))).Value;
548 var swarmConfiguration = (swarmConfigurationOptions ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions))).Value;
549 var internalConfiguration = (internalConfigurationOptions ?? throw new ArgumentNullException(nameof(internalConfigurationOptions))).Value;
550
551 ArgumentNullException.ThrowIfNull(logger);
552
553 logger.LogDebug("Database provider: {provider}", databaseConfiguration.DatabaseType);
554 logger.LogDebug("Content Root: {contentRoot}", hostingEnvironment.ContentRootPath);
555 logger.LogTrace("Web Root: {webRoot}", hostingEnvironment.WebRootPath);
556
557 // setup the HTTP request pipeline
558 // Add additional logging context to the request
559 applicationBuilder.UseAdditionalRequestLoggingContext(swarmConfiguration);
560
561 // Wrap exceptions in a 500 (ErrorMessage) response
562 applicationBuilder.UseServerErrorHandling();
563
564 // header forwarding important for OIDC
565 var forwardedHeaderOptions = new ForwardedHeadersOptions
566 {
567 ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost,
568 };
569
570 forwardedHeaderOptions.KnownNetworks.Clear();
571 forwardedHeaderOptions.KnownNetworks.Add(
572 new IPNetwork(
573 global::System.Net.IPAddress.Any,
574 0));
575
576 applicationBuilder.UseForwardedHeaders(forwardedHeaderOptions);
577
578 // metrics capture
579 applicationBuilder.UseHttpMetrics();
580
581 // Add the X-Powered-By response header
582 applicationBuilder.UseServerBranding(assemblyInformationProvider);
583
584 // Add the X-Accel-Buffering response header
585 applicationBuilder.UseDisabledNginxProxyBuffering();
586
587 // suppress OperationCancelledExceptions, they are just aborted HTTP requests
588 applicationBuilder.UseCancelledRequestSuppression();
589
590 // 503 requests made while the application is starting
591 applicationBuilder.UseAsyncInitialization<IInstanceManager>(
592 (instanceManager, cancellationToken) => instanceManager.Ready.WaitAsync(cancellationToken));
593
594 if (generalConfiguration.HostApiDocumentation)
595 {
596 var siteDocPath = Routes.ApiRoot + $"doc/{SwaggerConfiguration.DocumentName}.json";
597 if (!String.IsNullOrWhiteSpace(controlPanelConfiguration.PublicPath))
598 siteDocPath = controlPanelConfiguration.PublicPath.TrimEnd('/') + siteDocPath;
599
600 applicationBuilder.UseSwagger(options =>
601 {
602 options.RouteTemplate = Routes.ApiRoot + "doc/{documentName}.{json|yaml}";
603 });
604 applicationBuilder.UseSwaggerUI(options =>
605 {
607 options.SwaggerEndpoint(siteDocPath, "TGS API");
608 });
609 logger.LogTrace("Swagger API generation enabled");
610 }
611
612 // spa loading if necessary
613 if (controlPanelConfiguration.Enable)
614 {
615 logger.LogInformation("Web control panel enabled.");
616 applicationBuilder.UseFileServer(new FileServerOptions
617 {
619 EnableDefaultFiles = true,
620 EnableDirectoryBrowsing = false,
621 RedirectToAppendTrailingSlash = false,
622 });
623 }
624 else
625#if NO_WEBPANEL
626 logger.LogDebug("Web control panel was not included in TGS build!");
627#else
628 logger.LogTrace("Web control panel disabled!");
629#endif
630
631 // Enable endpoint routing
632 applicationBuilder.UseRouting();
633
634 // Set up CORS based on configuration if necessary
635 Action<CorsPolicyBuilder>? corsBuilder = null;
636 if (controlPanelConfiguration.AllowAnyOrigin)
637 {
638 logger.LogTrace("Access-Control-Allow-Origin: *");
639 corsBuilder = builder => builder.SetIsOriginAllowed(_ => true);
640 }
641 else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
642 {
643 logger.LogTrace("Access-Control-Allow-Origin: {allowedOrigins}", String.Join(',', controlPanelConfiguration.AllowedOrigins));
644 corsBuilder = builder => builder.WithOrigins([.. controlPanelConfiguration.AllowedOrigins]);
645 }
646
647 var originalBuilder = corsBuilder;
648 corsBuilder = builder =>
649 {
650 builder
651 .AllowAnyHeader()
652 .AllowAnyMethod()
653 .AllowCredentials()
654 .SetPreflightMaxAge(TimeSpan.FromDays(1));
655 originalBuilder?.Invoke(builder);
656 };
657 applicationBuilder.UseCors(corsBuilder);
658
659 // validate the API version
660 applicationBuilder.UseApiCompatibility();
661
662 // authenticate JWT tokens using our security pipeline if present, returns 401 if bad
663 applicationBuilder.UseAuthentication();
664
665 // enable authorization on endpoints
666 applicationBuilder.UseAuthorization();
667
668 // suppress and log database exceptions
669 applicationBuilder.UseDbConflictHandling();
670
671 // setup endpoints
672 applicationBuilder.UseEndpoints(endpoints =>
673 {
674 // access to the signalR jobs hub
675 endpoints.MapHub<JobsHub>(
677 options =>
678 {
679 options.Transports = HttpTransportType.ServerSentEvents;
680 options.CloseOnAuthenticationExpiration = true;
681 })
682 .RequireAuthorization()
683 .RequireCors(corsBuilder);
684
685 // majority of handling is done in the controllers
686 endpoints.MapControllers();
687
688 if (internalConfiguration.EnableGraphQL)
689 {
690 logger.LogWarning("Enabling GraphQL. This API is experimental and breaking changes may occur at any time!");
691 var gqlOptions = new GraphQLServerOptions
692 {
693 EnableBatching = true,
694 };
695
696 gqlOptions.Tool.Enable = generalConfiguration.HostApiDocumentation;
697
698 endpoints
699 .MapGraphQL(Routes.GraphQL)
700 .WithOptions(gqlOptions);
701 }
702
703 if (generalConfiguration.PrometheusPort.HasValue)
704 if (generalConfiguration.PrometheusPort == generalConfiguration.ApiPort)
705 {
706 endpoints.MapMetrics();
707 logger.LogDebug("Prometheus being hosted alongside server");
708 }
709 else
710 logger.LogDebug("Prometheus being hosted on port {prometheusPort}", generalConfiguration.PrometheusPort);
711 else
712 logger.LogTrace("Prometheus disabled");
713
714 endpoints.MapHealthChecks("/health");
715
716 var oidcConfig = securityConfigurationOptions.Value.OpenIDConnect;
717 if (oidcConfig == null)
718 return;
719
720 foreach (var kvp in oidcConfig)
721 endpoints.MapGet(
722 $"/oidc/{kvp.Key}/signin",
723 context => context.ChallengeAsync(
724 GetOidcScheme(kvp.Key),
725 new AuthenticationProperties
726 {
727 RedirectUri = $"/oidc/{kvp.Key}/landing",
728 }));
729 });
730
731 // 404 anything that gets this far
732 // End of request pipeline setup
733 logger.LogTrace("Configuration version: {configVersion}", GeneralConfiguration.CurrentConfigVersion);
734 logger.LogTrace("DMAPI Interop version: {interopVersion}", DMApiConstants.InteropVersion);
735 if (controlPanelConfiguration.Enable)
736 logger.LogTrace("Webpanel version: {webCPVersion}", MasterVersionsAttribute.Instance.RawWebpanelVersion);
737
738 logger.LogDebug("Starting hosting on port {httpApiPort}...", serverPortProvider.HttpApiPort);
739 }
740
742 protected override void ConfigureHostedService(IServiceCollection services)
743 => services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
744
750 void ConfigureAuthenticationPipeline(IServiceCollection services, SecurityConfiguration securityConfiguration)
751 {
752 services.AddHttpContextAccessor();
753 services.AddScoped<IApiHeadersProvider, ApiHeadersProvider>();
754 services.AddScoped<AuthenticationContextFactory>();
755 services.AddScoped<ITokenValidator>(provider => provider.GetRequiredService<AuthenticationContextFactory>());
756
758 services.AddScoped<Security.IAuthorizationService, AuthorizationService>();
759 services.AddScoped<IAuthorizationHandler, AuthorizationHandler>();
760
761 // what if you
762 // wanted to just do this:
763 // return provider.GetRequiredService<AuthenticationContextFactory>().CurrentAuthenticationContext
764 // But M$ said
765 // https://stackoverflow.com/questions/56792917/scoped-services-in-asp-net-core-with-signalr-hubs
766 services.AddScoped(provider => (provider
767 .GetRequiredService<IHttpContextAccessor>()
768 .HttpContext ?? throw new InvalidOperationException($"Unable to resolve {nameof(IAuthenticationContext)} due to no HttpContext being available!"))
769 .RequestServices
770 .GetRequiredService<AuthenticationContextFactory>()
771 .CurrentAuthenticationContext);
773
774 var authBuilder = services
775 .AddAuthentication(options =>
776 {
777 options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
778 })
779 .AddJwtBearer(jwtBearerOptions =>
780 {
781 // this line isn't actually run until the first request is made
782 // at that point tokenFactory will be populated
783 jwtBearerOptions.TokenValidationParameters = tokenFactory?.ValidationParameters ?? throw new InvalidOperationException("tokenFactory not initialized!");
784 jwtBearerOptions.MapInboundClaims = false;
785 jwtBearerOptions.Events = new JwtBearerEvents
786 {
787 OnMessageReceived = context =>
788 {
789 if (String.IsNullOrWhiteSpace(context.Token))
790 {
791 var accessToken = context.Request.Query["access_token"];
792 var path = context.HttpContext.Request.Path;
793
794 if (!String.IsNullOrWhiteSpace(accessToken) &&
795 path.StartsWithSegments(Routes.HubsRoot, StringComparison.OrdinalIgnoreCase))
796 {
797 context.Token = accessToken;
798 }
799 }
800
801 return Task.CompletedTask;
802 },
803 OnTokenValidated = context => context
804 .HttpContext
805 .RequestServices
806 .GetRequiredService<ITokenValidator>()
807 .ValidateTgsToken(
808 context,
809 context
810 .HttpContext
811 .RequestAborted),
812 };
813 });
814
815 services.AddAuthorization(options =>
816 {
817 options.AddPolicy(
819 builder => builder
820 .RequireAuthenticatedUser()
822
823 options.DefaultPolicy = options.GetPolicy(TgsAuthorizeAttribute.PolicyName)!;
824 });
825
826 var oidcConfig = securityConfiguration.OpenIDConnect;
827 if (oidcConfig == null || oidcConfig.Count == 0)
828 return;
829
830 authBuilder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
831
832 foreach (var kvp in oidcConfig)
833 {
834 var configName = kvp.Key;
835 authBuilder
836 .AddOpenIdConnect(
837 GetOidcScheme(configName),
838 options =>
839 {
840 var config = kvp.Value;
841
842 options.Authority = config.Authority?.ToString();
843 options.ClientId = config.ClientId;
844 options.ClientSecret = config.ClientSecret;
845
846 options.Events = new OpenIdConnectEvents
847 {
848 OnRemoteFailure = context =>
849 {
850 context.HandleResponse();
851 context.HttpContext.Response.Redirect($"{config.ReturnPath}?error={HttpUtility.UrlEncode(context.Failure?.Message ?? $"{options.Events.OnRemoteFailure} was called without an {nameof(Exception)}!")}&state=oidc.{HttpUtility.UrlEncode(configName)}");
852 return Task.CompletedTask;
853 },
854 OnTicketReceived = context =>
855 {
856 var services = context
857 .HttpContext
858 .RequestServices;
859 var tokenFactory = services
860 .GetRequiredService<ITokenFactory>();
861 var authenticationContext = services
862 .GetRequiredService<IAuthenticationContext>();
863 context.HandleResponse();
864 context.HttpContext.Response.Redirect($"{config.ReturnPath}?code={HttpUtility.UrlEncode(tokenFactory.CreateToken(authenticationContext.User, true))}&state=oidc.{HttpUtility.UrlEncode(configName)}");
865 return Task.CompletedTask;
866 },
867 };
868
869 Task CompleteAuth(RemoteAuthenticationContext<OpenIdConnectOptions> context)
870 => context
871 .HttpContext
872 .RequestServices
873 .GetRequiredService<ITokenValidator>()
874 .ValidateOidcToken(
875 context,
876 configName,
877 config.GroupIdClaim,
878 context
879 .HttpContext
880 .RequestAborted);
881
882 if (securityConfiguration.OidcStrictMode)
883 {
884 options.GetClaimsFromUserInfoEndpoint = true;
885 options.ClaimActions.MapUniqueJsonKey(config.GroupIdClaim, config.GroupIdClaim);
886 options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
887 {
888 NameClaimType = config.UsernameClaim,
889 RoleClaimType = "roles",
890 };
891
892 options.Scope.Add(OpenIdConnectScope.Profile);
893 options.Events.OnUserInformationReceived = CompleteAuth;
894 }
895 else
896 options.Events.OnTokenValidated = CompleteAuth;
897
898 options.Scope.Add(OpenIdConnectScope.OpenId);
899 options.Scope.Add(OpenIdConnectScope.OfflineAccess);
900
901 options.RequireHttpsMetadata = false;
902 options.SaveTokens = true;
903 options.ResponseType = OpenIdConnectResponseType.Code;
904 options.MapInboundClaims = false;
905
906 options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
907
908 var basePath = $"/oidc/{configName}/";
909 options.CallbackPath = new PathString(basePath + "signin-callback");
910 options.SignedOutCallbackPath = new PathString(basePath + "signout-callback");
911 options.RemoteSignOutPath = new PathString(basePath + "signout");
912 });
913 }
914 }
915 }
916}
Represents the header that must be present for every server request.
Definition ApiHeaders.cs:25
Routes to a server actions.
Definition Routes.cs:9
const string GraphQL
The GraphQL route.
Definition Routes.cs:18
const string HubsRoot
The root route of all hubs.
Definition Routes.cs:23
const string JobsHub
The root route of all hubs.
Definition Routes.cs:118
Base implementation of IEngineInstaller for EngineType.Byond.
Implementation of IEngineInstaller that forwards calls to different IEngineInstaller based on their a...
Implementation of IEngineInstaller for EngineType.OpenDream.
Implementation of OpenDreamInstaller for Windows systems.
Constants used for communication with the DMAPI.
static readonly Version InteropVersion
The DMAPI InteropVersion being used.
DatabaseType DatabaseType
The Configuration.DatabaseType to create.
LogLevel MicrosoftLogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs for Microsoft library sources.
string GetFullLogDirectory(IIOManager ioManager, IAssemblyInformationProvider assemblyInformationProvider, IPlatformIdentifier platformIdentifier)
Gets the evaluated log Directory.
LogLevel LogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs.
static readonly Version CurrentConfigVersion
The current ConfigVersion.
bool HostApiDocumentation
If the swagger documentation and UI should be made avaiable.
uint RestartTimeoutMinutes
The timeout minutes for restarting the server.
ushort? PrometheusPort
The port Prometheus metrics are published on, if any.
Unstable configuration options used internally by TGS.
bool UsingSystemD
If the server is running under SystemD.
Configuration options pertaining to user security.
IDictionary< string, OidcConfiguration >? OpenIDConnect
OIDC provider settings keyed by scheme name.
bool OidcStrictMode
If OIDC strict mode should be enabled. This mode enforces the existence of at least one OpenIDConnect...
Configuration options for the game sessions.
Configuration for the server swarm system.
Configuration for the automatic update system.
const string ControlPanelRoute
Route to the ControlPanelController.
Backend abstract implementation of IDatabaseContext.
IIOManager that resolves paths to Environment.CurrentDirectory.
IPostWriteHandler for POSIX systems.
IPostWriteHandler for Windows systems.
Handles mapping groups for the JobsHub.
A SignalR Hub for pushing job updates.
Definition JobsHub.cs:16
Attribute for bringing in the master versions list from MSBuild that aren't embedded into assemblies ...
string RawWebpanelVersion
The Version string of the control panel version built.
static MasterVersionsAttribute Instance
Return the Assembly's instance of the MasterVersionsAttribute.
A IClaimsTransformation that maps Claims using an IAuthenticationContext.
An IHubFilter that denies method calls and connections if the IAuthenticationContext is not valid for...
IAuthorizationHandler for RightsConditional<TRights>s and UserSessionValidRequirements.
Helper for using the AuthorizeAttribute with the Api.Rights system.
const string PolicyName
Policy used to apply global requirement of UserEnabledRole.
const string UserEnabledRole
Role used to indicate access to the server is allowed.
ISystemIdentityFactory for windows systems. Uses long running tasks due to potential networked domain...
Implementation of IServerFactory.
DI root for configuring a SetupWizard.
Helps keep servers connected to the same database in sync by coordinating updates.
Implements the SystemD notify service protocol.
Implementation of the file transfer service.
Helpers for manipulating the Serilog.Context.LogContext.
static string Template
Common template used for adding our custom log context to serilog.
Implements various filters for Swashbuckle.
const string DocumentationSiteRouteExtension
The path to the hosted documentation site.
static void Configure(SwaggerGenOptions swaggerGenOptions, string assemblyDocumentationPath, string apiDocumentationPath)
Configure the swagger settings.
JsonConverter and IYamlTypeConverter for serializing global::System.Versions in semver format.
SignalR client methods for receiving JobResponses.
Definition IJobsHub.cs:12
IAuthority for administrative server operations.
IAuthority for manipulating chat bots.
IAuthority for authenticating with the server.
Invokes TAuthority methods and generates IActionResult responses.
For downloading and installing game engines for a given system.
Task Ready
Task that completes when the IInstanceManager finishes initializing.
For low level interactions with a LibGit2Sharp.IRepository.
Factory for scoping usage of IDatabaseContexts. Meant for use by Components.
For initially setting up a database.
Implementation of HotChocolate.Subscriptions.ITopicEventReceiver that works around the global::System...
Interface for using filesystems.
Definition IIOManager.cs:14
string ConcatPath(params string[] paths)
Combines an array of strings into a path.
string GetDirectoryName(string path)
Gets the directory portion of a given path .
string GetFileNameWithoutExtension(string path)
Gets the file name portion of a path with.
IIOManager CreateResolverForSubdirectory(string subdirectoryPath)
Create a new IIOManager that resolves paths to the specified subdirectoryPath .
Handles changing file modes/permissions after writing.
For accessing the disk in a synchronous manner.
Manages the runtime of Jobs.
The service that manages everything to do with jobs.
Definition IJobService.cs:9
Allows manually triggering jobs hub updates.
For creating and accessing authentication contexts.
Interface for accessing the current request's ClaimsPrincipal.
Contains various cryptographic functions.
Receives notifications about permissions updates.
TokenValidationParameters ValidationParameters
The TokenValidationParameters for the ITokenFactory.
Handles validating authentication tokens.
Set of objects needed to configure an Core.Application.
GeneralConfiguration GeneralConfiguration
The Configuration.GeneralConfiguration.
ElasticsearchConfiguration ElasticsearchConfiguration
The Configuration.ElasticsearchConfiguration.
FileLoggingConfiguration FileLoggingConfiguration
The Configuration.FileLoggingConfiguration.
DatabaseConfiguration DatabaseConfiguration
The Configuration.DatabaseConfiguration.
SecurityConfiguration SecurityConfiguration
The Configuration.SecurityConfiguration.
InternalConfiguration InternalConfiguration
The Configuration.InternalConfiguration.
IPlatformIdentifier PlatformIdentifier
The IPlatformIdentifier.
Swarm service operations for the Controllers.SwarmController.
Start and stop controllers for a swarm service.
Used for swarm operations. Functions may be no-op based on configuration.
ProductInfoHeaderValue ProductInfoHeaderValue
The ProductInfoHeaderValue for the assembly.
Service for managing the dotnet-dump installation.
On Windows, DreamDaemon will show an unskippable prompt when using /world/proc/OpenPort()....
bool IsWindows
If the current platform is a Windows platform.
Abstraction for suspending and resuming processes.
Reads and writes to Streams associated with FileTicketResponses.
Service for temporarily storing files to be downloaded or uploaded.
Gets unassigned ports for use by TGS.
EngineType
The type of engine the codebase is using.
Definition EngineType.cs:7
@ Configuration
ConfigurationRights.
DatabaseType
Type of database to user.