tgstation-server 6.16.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.Threading.Tasks;
6using System.Web;
7
8using Cyberboss.AspNetCore.AsyncInitializer;
9
10using Elastic.CommonSchema.Serilog;
11
12using HotChocolate.AspNetCore;
13using HotChocolate.Subscriptions;
14using HotChocolate.Types;
15
16using Microsoft.AspNetCore.Authentication;
17using Microsoft.AspNetCore.Authentication.Cookies;
18using Microsoft.AspNetCore.Authentication.JwtBearer;
19using Microsoft.AspNetCore.Authentication.OpenIdConnect;
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
82
84{
88#pragma warning disable CA1506
89 public sealed class Application : SetupApplication
90 {
94 readonly IWebHostEnvironment hostingEnvironment;
95
99 ITokenFactory? tokenFactory;
100
105 public static IServerFactory CreateDefaultServerFactory()
106 {
107 var assemblyInformationProvider = new AssemblyInformationProvider();
108 var ioManager = new DefaultIOManager();
109 return new ServerFactory(
110 assemblyInformationProvider,
111 ioManager);
112 }
113
120 static void AddWatchdog<TSystemWatchdogFactory>(IServiceCollection services, IPostSetupServices postSetupServices)
121 where TSystemWatchdogFactory : class, IWatchdogFactory
122 {
123 if (postSetupServices.GeneralConfiguration.UseBasicWatchdog)
124 services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
125 else
126 services.AddSingleton<IWatchdogFactory, TSystemWatchdogFactory>();
127 }
128
134 static string GetOidcScheme(string schemeKey)
135 => AuthenticationContextFactory.OpenIDConnectAuthenticationSchemePrefix + schemeKey;
136
142 public Application(
143 IConfiguration configuration,
144 IWebHostEnvironment hostingEnvironment)
145 : base(configuration)
146 {
147 this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
148 }
149
157 public void ConfigureServices(
158 IServiceCollection services,
159 IAssemblyInformationProvider assemblyInformationProvider,
160 IIOManager ioManager,
161 IPostSetupServices postSetupServices)
162 {
163 ConfigureServices(services, assemblyInformationProvider, ioManager);
164
165 ArgumentNullException.ThrowIfNull(postSetupServices);
166
167 // configure configuration
168 services.UseStandardConfig<UpdatesConfiguration>(Configuration);
169 services.UseStandardConfig<ControlPanelConfiguration>(Configuration);
170 services.UseStandardConfig<SwarmConfiguration>(Configuration);
171 services.UseStandardConfig<SessionConfiguration>(Configuration);
172 services.UseStandardConfig<TelemetryConfiguration>(Configuration);
173
174 // enable options which give us config reloading
175 services.AddOptions();
176
177 // Set the timeout for IHostedService.StopAsync
178 services.Configure<HostOptions>(
179 opts => opts.ShutdownTimeout = TimeSpan.FromMinutes(postSetupServices.GeneralConfiguration.RestartTimeoutMinutes));
180
181 static LogEventLevel? ConvertSeriLogLevel(LogLevel logLevel) =>
182 logLevel switch
183 {
184 LogLevel.Critical => LogEventLevel.Fatal,
185 LogLevel.Debug => LogEventLevel.Debug,
186 LogLevel.Error => LogEventLevel.Error,
187 LogLevel.Information => LogEventLevel.Information,
188 LogLevel.Trace => LogEventLevel.Verbose,
189 LogLevel.Warning => LogEventLevel.Warning,
190 LogLevel.None => null,
191 _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel)),
192 };
193
194 var microsoftEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.MicrosoftLogLevel);
195 var elasticsearchConfiguration = postSetupServices.ElasticsearchConfiguration;
196 services.SetupLogging(
197 config =>
198 {
199 if (microsoftEventLevel.HasValue)
200 {
201 config.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
202 config.MinimumLevel.Override("System.Net.Http.HttpClient", microsoftEventLevel.Value);
203 }
204 },
205 sinkConfig =>
206 {
207 if (postSetupServices.FileLoggingConfiguration.Disable)
208 return;
209
210 var logPath = postSetupServices.FileLoggingConfiguration.GetFullLogDirectory(
211 ioManager,
212 assemblyInformationProvider,
213 postSetupServices.PlatformIdentifier);
214
215 var logEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.LogLevel);
216
217 var formatter = new MessageTemplateTextFormatter(
218 "{Timestamp:o} "
220 + "): [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}",
221 null);
222
223 logPath = ioManager.ConcatPath(logPath, "tgs-.log");
224 var rollingFileConfig = sinkConfig.File(
225 formatter,
226 logPath,
227 logEventLevel ?? LogEventLevel.Verbose,
228 50 * 1024 * 1024, // 50MB max size
229 flushToDiskInterval: TimeSpan.FromSeconds(2),
230 rollingInterval: RollingInterval.Day,
231 rollOnFileSizeLimit: true);
232 },
233 elasticsearchConfiguration.Enable
234 ? new ElasticsearchSinkOptions(elasticsearchConfiguration.Host ?? throw new InvalidOperationException($"Missing {ElasticsearchConfiguration.Section}:{nameof(elasticsearchConfiguration.Host)}!"))
235 {
236 // Yes I know this means they cannot use a self signed cert unless they also have authentication, but lets be real here
237 // No one is going to be doing one of those but not the other
238 ModifyConnectionSettings = connectionConfigration => (!String.IsNullOrWhiteSpace(elasticsearchConfiguration.Username) && !String.IsNullOrWhiteSpace(elasticsearchConfiguration.Password))
239 ? connectionConfigration
240 .BasicAuthentication(
241 elasticsearchConfiguration.Username,
242 elasticsearchConfiguration.Password)
243 .ServerCertificateValidationCallback((o, certificate, chain, errors) => true)
244 : null,
245 CustomFormatter = new EcsTextFormatter(),
246 AutoRegisterTemplate = true,
247 AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7,
248 IndexFormat = "tgs-logs",
249 }
250 : null,
251 postSetupServices.InternalConfiguration,
252 postSetupServices.FileLoggingConfiguration);
253
254 // configure authentication pipeline
255 ConfigureAuthenticationPipeline(services, postSetupServices.SecurityConfiguration);
256
257 // add mvc, configure the json serializer settings
258 var jsonVersionConverterList = new List<JsonConverter>
259 {
260 new VersionConverter(),
261 };
262
263 void ConfigureNewtonsoftJsonSerializerSettingsForApi(JsonSerializerSettings settings)
264 {
265 settings.NullValueHandling = NullValueHandling.Ignore;
266 settings.CheckAdditionalContent = true;
267 settings.MissingMemberHandling = MissingMemberHandling.Error;
268 settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
269 settings.Converters = jsonVersionConverterList;
270 }
271
272 services
273 .AddMvc(options =>
274 {
275 options.ReturnHttpNotAcceptable = true;
276 options.RespectBrowserAcceptHeader = true;
277 })
278 .AddNewtonsoftJson(options =>
279 {
280 options.AllowInputFormatterExceptionMessages = true;
281 ConfigureNewtonsoftJsonSerializerSettingsForApi(options.SerializerSettings);
282 });
283
284 services.AddSignalR(
285 options =>
286 {
287 options.AddFilter<AuthorizationContextHubFilter>();
288 })
289 .AddNewtonsoftJsonProtocol(options =>
290 {
291 ConfigureNewtonsoftJsonSerializerSettingsForApi(options.PayloadSerializerSettings);
292 });
293
294 services.AddHub<JobsHub, IJobsHub>();
295
296 if (postSetupServices.GeneralConfiguration.HostApiDocumentation)
297 {
298 string GetDocumentationFilePath(string assemblyLocation) => ioManager.ConcatPath(ioManager.GetDirectoryName(assemblyLocation), String.Concat(ioManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
299 var assemblyDocumentationPath = GetDocumentationFilePath(GetType().Assembly.Location);
300 var apiDocumentationPath = GetDocumentationFilePath(typeof(ApiHeaders).Assembly.Location);
301 services.AddSwaggerGen(genOptions => SwaggerConfiguration.Configure(genOptions, assemblyDocumentationPath, apiDocumentationPath));
302 services.AddSwaggerGenNewtonsoftSupport();
303 }
304
305 // CORS conditionally enabled later
306 services.AddCors();
307
308 // Enable managed HTTP clients
309 services.AddHttpClient();
311
312 // configure metrics
313 var prometheusPort = postSetupServices.GeneralConfiguration.PrometheusPort;
314
315 services.AddSingleton<IMetricFactory>(_ => Metrics.DefaultFactory);
316 services.AddSingleton<ICollectorRegistry>(_ => Metrics.DefaultRegistry);
317
318 if (prometheusPort.HasValue && prometheusPort != postSetupServices.GeneralConfiguration.ApiPort)
319 services.AddMetricServer(options => options.Port = prometheusPort.Value);
320
321 services.UseHttpClientMetrics();
322
323 var healthChecksBuilder = services
324 .AddHealthChecks()
325 .ForwardToPrometheus();
326
327 // configure graphql
328 services
329 .AddScoped<GraphQL.Subscriptions.ITopicEventReceiver, ShutdownAwareTopicEventReceiver>()
330 .AddGraphQLServer()
331 .AddAuthorization(
332 options =>
333 {
334 options.AddPolicy(
336 builder => builder
337 .RequireAuthenticatedUser()
339 options.AddPolicy(
340 "testingasdf",
341 builder =>
342 {
343 builder.RequireAuthenticatedUser();
344 builder.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme);
345 });
346 })
347 .ModifyOptions(options =>
348 {
349 options.EnsureAllNodesCanBeResolved = true;
350 options.EnableFlagEnums = true;
351 })
352#if DEBUG
353 .ModifyCostOptions(options =>
354 {
355 options.EnforceCostLimits = false;
356 })
357#endif
358 .AddMutationConventions()
359 .AddInMemorySubscriptions(
360 new SubscriptionOptions
361 {
362 TopicBufferCapacity = 1024, // mainly so high for tests, not possible to DoS the server without authentication and some other access to generate messages
363 })
364 .AddGlobalObjectIdentification()
365 .AddQueryFieldToMutationPayloads()
366 .ModifyOptions(options =>
367 {
368 options.EnableDefer = true;
369 })
370 .ModifyPagingOptions(pagingOptions =>
371 {
372 pagingOptions.IncludeTotalCount = true;
373 pagingOptions.RequirePagingBoundaries = false;
374 pagingOptions.DefaultPageSize = ApiController.DefaultPageSize;
375 pagingOptions.MaxPageSize = ApiController.MaximumPageSize;
376 })
377 .AddFiltering()
378 .AddSorting()
379 .AddHostTypes()
380 .AddErrorFilter<ErrorMessageFilter>()
381 .AddType<StandaloneNode>()
382 .AddType<LocalGateway>()
383 .AddType<RemoteGateway>()
384 .AddType<GraphQL.Types.UserName>()
385 .AddType<UnsignedIntType>()
386 .BindRuntimeType<Version, SemverType>()
387 .TryAddTypeInterceptor<RightsTypeInterceptor>()
388 .AddQueryType<Query>()
389 .AddMutationType<Mutation>()
390 .AddSubscriptionType<Subscription>();
391
392 void AddTypedContext<TContext>()
393 where TContext : DatabaseContext
394 {
395 var configureAction = DatabaseContext.GetConfigureAction<TContext>();
396
397 services.AddDbContextPool<TContext>((serviceProvider, builder) =>
398 {
399 if (hostingEnvironment.IsDevelopment())
400 builder.EnableSensitiveDataLogging();
401
402 var databaseConfigOptions = serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>();
403 var databaseConfig = databaseConfigOptions.Value ?? throw new InvalidOperationException("DatabaseConfiguration missing!");
404 configureAction(builder, databaseConfig);
405 });
406 services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
407
408 healthChecksBuilder
409 .AddDbContextCheck<TContext>();
410 }
411
412 // add the correct database context type
413 var dbType = postSetupServices.DatabaseConfiguration.DatabaseType;
414 switch (dbType)
415 {
416 case DatabaseType.MySql:
417 case DatabaseType.MariaDB:
418 AddTypedContext<MySqlDatabaseContext>();
419 break;
420 case DatabaseType.SqlServer:
421 AddTypedContext<SqlServerDatabaseContext>();
422 break;
423 case DatabaseType.Sqlite:
424 AddTypedContext<SqliteDatabaseContext>();
425 break;
426 case DatabaseType.PostgresSql:
427 AddTypedContext<PostgresSqlDatabaseContext>();
428 break;
429 default:
430 throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
431 }
432
433 // configure other database services
434 services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
435 services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
436
437 // configure other security services
438 services.AddSingleton<IOAuthProviders, OAuthProviders>();
439 services.AddSingleton<IIdentityCache, IdentityCache>();
440 services.AddSingleton<ICryptographySuite, CryptographySuite>();
441 services.AddSingleton<ITokenFactory, TokenFactory>();
443 services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
444
445 // configure platform specific services
446 if (postSetupServices.PlatformIdentifier.IsWindows)
447 {
448 AddWatchdog<WindowsWatchdogFactory>(services, postSetupServices);
451 services.AddSingleton<ByondInstallerBase, WindowsByondInstaller>();
452 services.AddSingleton<OpenDreamInstaller, WindowsOpenDreamInstaller>();
453 services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
454 services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
455
456 services.AddSingleton<WindowsNetworkPromptReaper>();
457 services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
458 services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
459 }
460 else
461 {
462 AddWatchdog<PosixWatchdogFactory>(services, postSetupServices);
463 services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
464 services.AddSingleton<IFilesystemLinkFactory, PosixFilesystemLinkFactory>();
465 services.AddSingleton<ByondInstallerBase, PosixByondInstaller>();
466 services.AddSingleton<OpenDreamInstaller>();
467 services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
468
469 services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
470 services.AddHostedService<PosixProcessFeatures>();
471
472 // PosixProcessFeatures also needs a IProcessExecutor for gcore
473 services.AddSingleton(x => new Lazy<IProcessExecutor>(() => x.GetRequiredService<IProcessExecutor>(), true));
474 services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
475
476 services.AddHostedService<PosixSignalHandler>();
477 }
478
479 // only global repo manager should be for the OD repo
480 // god help me if we need more
481 var openDreamRepositoryDirectory = ioManager.ConcatPath(
482 ioManager.GetPathInLocalDirectory(assemblyInformationProvider),
483 "OpenDreamRepository");
484 services.AddSingleton(
485 services => services
486 .GetRequiredService<IRepositoryManagerFactory>()
487 .CreateRepositoryManager(
489 services.GetRequiredService<IIOManager>(),
490 openDreamRepositoryDirectory),
491 new NoopEventConsumer()));
492
493 services.AddSingleton(
494 serviceProvider => new Dictionary<EngineType, IEngineInstaller>
495 {
496 { EngineType.Byond, serviceProvider.GetRequiredService<ByondInstallerBase>() },
497 { EngineType.OpenDream, serviceProvider.GetRequiredService<OpenDreamInstaller>() },
498 }
499 .ToFrozenDictionary());
500 services.AddSingleton<IEngineInstaller, DelegatingEngineInstaller>();
501
502 if (postSetupServices.InternalConfiguration.UsingSystemD)
503 services.AddHostedService<SystemDManager>();
504
505 // configure file transfer services
506 services.AddSingleton<FileTransferService>();
507 services.AddSingleton<IFileTransferStreamHandler>(x => x.GetRequiredService<FileTransferService>());
508 services.AddSingleton<IFileTransferTicketProvider>(x => x.GetRequiredService<FileTransferService>());
510
511 // configure swarm service
512 services.AddSingleton<SwarmService>();
513 services.AddSingleton<ISwarmService>(x => x.GetRequiredService<SwarmService>());
514 services.AddSingleton<ISwarmOperations>(x => x.GetRequiredService<SwarmService>());
515 services.AddSingleton<ISwarmServiceController>(x => x.GetRequiredService<SwarmService>());
516
517 // configure component services
518 services.AddSingleton<IPortAllocator, PortAllocator>();
519 services.AddSingleton<IInstanceFactory, InstanceFactory>();
520 services.AddSingleton<IGitRemoteFeaturesFactory, GitRemoteFeaturesFactory>();
521 services.AddSingleton<ILibGit2RepositoryFactory, LibGit2RepositoryFactory>();
522 services.AddSingleton<ILibGit2Commands, LibGit2Commands>();
523 services.AddSingleton<IRepositoryManagerFactory, RepostoryManagerFactory>();
525 services.AddChatProviderFactory();
526 services.AddSingleton<IChatManagerFactory, ChatManagerFactory>();
527 services.AddSingleton<IServerUpdater, ServerUpdater>();
528 services.AddSingleton<IServerUpdateInitiator, ServerUpdateInitiator>();
529 services.AddSingleton<IDotnetDumpService, DotnetDumpService>();
530
531 // configure authorities
532 services.AddScoped(typeof(IRestAuthorityInvoker<>), typeof(RestAuthorityInvoker<>));
533 services.AddScoped(typeof(IGraphQLAuthorityInvoker<>), typeof(GraphQLAuthorityInvoker<>));
534 services.AddScoped<ILoginAuthority, LoginAuthority>();
535 services.AddScoped<IUserAuthority, UserAuthority>();
536 services.AddScoped<IUserGroupAuthority, UserGroupAuthority>();
537 services.AddScoped<IPermissionSetAuthority, PermissionSetAuthority>();
539
540 // configure misc services
541 services.AddSingleton<IProcessExecutor, ProcessExecutor>();
542 services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
543 services.AddSingleton<IServerPortProvider, ServerPortProivder>();
544 services.AddSingleton<ITopicClientFactory, TopicClientFactory>();
545 services.AddHostedService<CommandPipeManager>();
546 services.AddHostedService<VersionReportingService>();
547
548 services.AddFileDownloader();
549 services.AddGitHub();
550
551 // configure root services
552 services.AddSingleton<JobService>();
553 services.AddSingleton<IJobService>(provider => provider.GetRequiredService<JobService>());
554 services.AddSingleton<IJobsHubUpdater>(provider => provider.GetRequiredService<JobService>());
555 services.AddSingleton<IJobManager>(x => x.GetRequiredService<IJobService>());
556 services.AddSingleton<JobsHubGroupMapper>();
557 services.AddSingleton<IPermissionsUpdateNotifyee>(provider => provider.GetRequiredService<JobsHubGroupMapper>());
558 services.AddSingleton<IHostedService>(x => x.GetRequiredService<JobsHubGroupMapper>()); // bit of a hack, but we need this to load immediated
559
560 services.AddSingleton<InstanceManager>();
561 services.AddSingleton<IBridgeDispatcher>(x => x.GetRequiredService<InstanceManager>());
562 services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
563 }
564
580 public void Configure(
581 IApplicationBuilder applicationBuilder,
582 IServerControl serverControl,
583 ITokenFactory tokenFactory,
584 IServerPortProvider serverPortProvider,
585 IAssemblyInformationProvider assemblyInformationProvider,
586 IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
587 IOptions<GeneralConfiguration> generalConfigurationOptions,
588 IOptions<DatabaseConfiguration> databaseConfigurationOptions,
589 IOptions<SecurityConfiguration> securityConfigurationOptions,
590 IOptions<SwarmConfiguration> swarmConfigurationOptions,
591 IOptions<InternalConfiguration> internalConfigurationOptions,
592 ILogger<Application> logger)
593 {
594 ArgumentNullException.ThrowIfNull(applicationBuilder);
595 ArgumentNullException.ThrowIfNull(serverControl);
596
597 this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
598
599 ArgumentNullException.ThrowIfNull(serverPortProvider);
600 ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
601
602 var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
603 var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
604 var databaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
605 var swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
606 var internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions));
607
608 ArgumentNullException.ThrowIfNull(logger);
609
610 logger.LogDebug("Database provider: {provider}", databaseConfiguration.DatabaseType);
611 logger.LogDebug("Content Root: {contentRoot}", hostingEnvironment.ContentRootPath);
612 logger.LogTrace("Web Root: {webRoot}", hostingEnvironment.WebRootPath);
613
614 // setup the HTTP request pipeline
615 // Add additional logging context to the request
616 applicationBuilder.UseAdditionalRequestLoggingContext(swarmConfiguration);
617
618 // Wrap exceptions in a 500 (ErrorMessage) response
619 applicationBuilder.UseServerErrorHandling();
620
621 // header forwarding important for OIDC
622 var forwardedHeaderOptions = new ForwardedHeadersOptions
623 {
624 ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost,
625 };
626
627 forwardedHeaderOptions.KnownNetworks.Clear();
628 forwardedHeaderOptions.KnownNetworks.Add(
629 new IPNetwork(
630 global::System.Net.IPAddress.Any,
631 0));
632
633 applicationBuilder.UseForwardedHeaders(forwardedHeaderOptions);
634
635 // metrics capture
636 applicationBuilder.UseHttpMetrics();
637
638 // Add the X-Powered-By response header
639 applicationBuilder.UseServerBranding(assemblyInformationProvider);
640
641 // Add the X-Accel-Buffering response header
642 applicationBuilder.UseDisabledNginxProxyBuffering();
643
644 // suppress OperationCancelledExceptions, they are just aborted HTTP requests
645 applicationBuilder.UseCancelledRequestSuppression();
646
647 // 503 requests made while the application is starting
648 applicationBuilder.UseAsyncInitialization<IInstanceManager>(
649 (instanceManager, cancellationToken) => instanceManager.Ready.WaitAsync(cancellationToken));
650
651 if (generalConfiguration.HostApiDocumentation)
652 {
653 var siteDocPath = Routes.ApiRoot + $"doc/{SwaggerConfiguration.DocumentName}.json";
654 if (!String.IsNullOrWhiteSpace(controlPanelConfiguration.PublicPath))
655 siteDocPath = controlPanelConfiguration.PublicPath.TrimEnd('/') + siteDocPath;
656
657 applicationBuilder.UseSwagger(options =>
658 {
659 options.RouteTemplate = Routes.ApiRoot + "doc/{documentName}.{json|yaml}";
660 });
661 applicationBuilder.UseSwaggerUI(options =>
662 {
664 options.SwaggerEndpoint(siteDocPath, "TGS API");
665 });
666 logger.LogTrace("Swagger API generation enabled");
667 }
668
669 // spa loading if necessary
670 if (controlPanelConfiguration.Enable)
671 {
672 logger.LogInformation("Web control panel enabled.");
673 applicationBuilder.UseFileServer(new FileServerOptions
674 {
676 EnableDefaultFiles = true,
677 EnableDirectoryBrowsing = false,
678 RedirectToAppendTrailingSlash = false,
679 });
680 }
681 else
682#if NO_WEBPANEL
683 logger.LogDebug("Web control panel was not included in TGS build!");
684#else
685 logger.LogTrace("Web control panel disabled!");
686#endif
687
688 // Enable endpoint routing
689 applicationBuilder.UseRouting();
690
691 // Set up CORS based on configuration if necessary
692 Action<CorsPolicyBuilder>? corsBuilder = null;
693 if (controlPanelConfiguration.AllowAnyOrigin)
694 {
695 logger.LogTrace("Access-Control-Allow-Origin: *");
696 corsBuilder = builder => builder.SetIsOriginAllowed(_ => true);
697 }
698 else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
699 {
700 logger.LogTrace("Access-Control-Allow-Origin: {allowedOrigins}", String.Join(',', controlPanelConfiguration.AllowedOrigins));
701 corsBuilder = builder => builder.WithOrigins([.. controlPanelConfiguration.AllowedOrigins]);
702 }
703
704 var originalBuilder = corsBuilder;
705 corsBuilder = builder =>
706 {
707 builder
708 .AllowAnyHeader()
709 .AllowAnyMethod()
710 .AllowCredentials()
711 .SetPreflightMaxAge(TimeSpan.FromDays(1));
712 originalBuilder?.Invoke(builder);
713 };
714 applicationBuilder.UseCors(corsBuilder);
715
716 // validate the API version
717 applicationBuilder.UseApiCompatibility();
718
719 // authenticate JWT tokens using our security pipeline if present, returns 401 if bad
720 applicationBuilder.UseAuthentication();
721
722 // enable authorization on endpoints
723 applicationBuilder.UseAuthorization();
724
725 // suppress and log database exceptions
726 applicationBuilder.UseDbConflictHandling();
727
728 // setup endpoints
729 applicationBuilder.UseEndpoints(endpoints =>
730 {
731 // access to the signalR jobs hub
732 endpoints.MapHub<JobsHub>(
734 options =>
735 {
736 options.Transports = HttpTransportType.ServerSentEvents;
737 options.CloseOnAuthenticationExpiration = true;
738 })
739 .RequireAuthorization()
740 .RequireCors(corsBuilder);
741
742 // majority of handling is done in the controllers
743 endpoints.MapControllers();
744
745 if (internalConfiguration.EnableGraphQL)
746 {
747 logger.LogWarning("Enabling GraphQL. This API is experimental and breaking changes may occur at any time!");
748 var gqlOptions = new GraphQLServerOptions
749 {
750 EnableBatching = true,
751 };
752
753 gqlOptions.Tool.Enable = generalConfiguration.HostApiDocumentation;
754
755 endpoints
756 .MapGraphQL(Routes.GraphQL)
757 .WithOptions(gqlOptions);
758 }
759
760 if (generalConfiguration.PrometheusPort.HasValue)
761 if (generalConfiguration.PrometheusPort == generalConfiguration.ApiPort)
762 {
763 endpoints.MapMetrics();
764 logger.LogDebug("Prometheus being hosted alongside server");
765 }
766 else
767 logger.LogDebug("Prometheus being hosted on port {prometheusPort}", generalConfiguration.PrometheusPort);
768 else
769 logger.LogTrace("Prometheus disabled");
770
771 endpoints.MapHealthChecks("/health");
772
773 var oidcConfig = securityConfigurationOptions.Value.OpenIDConnect;
774 if (oidcConfig == null)
775 return;
776
777 foreach (var kvp in oidcConfig)
778 endpoints.MapGet(
779 $"/oidc/{kvp.Key}/signin",
780 context => context.ChallengeAsync(
781 GetOidcScheme(kvp.Key),
782 new AuthenticationProperties
783 {
784 RedirectUri = $"/oidc/{kvp.Key}/landing",
785 }));
786 });
787
788 // 404 anything that gets this far
789 // End of request pipeline setup
790 logger.LogTrace("Configuration version: {configVersion}", GeneralConfiguration.CurrentConfigVersion);
791 logger.LogTrace("DMAPI Interop version: {interopVersion}", DMApiConstants.InteropVersion);
792 if (controlPanelConfiguration.Enable)
793 logger.LogTrace("Webpanel version: {webCPVersion}", MasterVersionsAttribute.Instance.RawWebpanelVersion);
794
795 logger.LogDebug("Starting hosting on port {httpApiPort}...", serverPortProvider.HttpApiPort);
796 }
797
799 protected override void ConfigureHostedService(IServiceCollection services)
800 => services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
801
807 void ConfigureAuthenticationPipeline(IServiceCollection services, SecurityConfiguration securityConfiguration)
808 {
809 services.AddHttpContextAccessor();
810 services.AddScoped<IApiHeadersProvider, ApiHeadersProvider>();
811 services.AddScoped<AuthenticationContextFactory>();
812 services.AddScoped<ITokenValidator>(provider => provider.GetRequiredService<AuthenticationContextFactory>());
813
814 // what if you
815 // wanted to just do this:
816 // return provider.GetRequiredService<AuthenticationContextFactory>().CurrentAuthenticationContext
817 // But M$ said
818 // https://stackoverflow.com/questions/56792917/scoped-services-in-asp-net-core-with-signalr-hubs
819 services.AddScoped(provider => (provider
820 .GetRequiredService<IHttpContextAccessor>()
821 .HttpContext ?? throw new InvalidOperationException($"Unable to resolve {nameof(IAuthenticationContext)} due to no HttpContext being available!"))
822 .RequestServices
823 .GetRequiredService<AuthenticationContextFactory>()
824 .CurrentAuthenticationContext);
826
827 var authBuilder = services
828 .AddAuthentication(options =>
829 {
830 options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
831 })
832 .AddJwtBearer(jwtBearerOptions =>
833 {
834 // this line isn't actually run until the first request is made
835 // at that point tokenFactory will be populated
836 jwtBearerOptions.TokenValidationParameters = tokenFactory?.ValidationParameters ?? throw new InvalidOperationException("tokenFactory not initialized!");
837 jwtBearerOptions.MapInboundClaims = false;
838 jwtBearerOptions.Events = new JwtBearerEvents
839 {
840 OnMessageReceived = context =>
841 {
842 if (String.IsNullOrWhiteSpace(context.Token))
843 {
844 var accessToken = context.Request.Query["access_token"];
845 var path = context.HttpContext.Request.Path;
846
847 if (!String.IsNullOrWhiteSpace(accessToken) &&
848 path.StartsWithSegments(Routes.HubsRoot, StringComparison.OrdinalIgnoreCase))
849 {
850 context.Token = accessToken;
851 }
852 }
853
854 return Task.CompletedTask;
855 },
856 OnTokenValidated = context => context
857 .HttpContext
858 .RequestServices
859 .GetRequiredService<ITokenValidator>()
860 .ValidateTgsToken(
861 context,
862 context
863 .HttpContext
864 .RequestAborted),
865 };
866 });
867
868 var oidcConfig = securityConfiguration.OpenIDConnect;
869 if (oidcConfig == null || oidcConfig.Count == 0)
870 return;
871
872 authBuilder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
873
874 foreach (var kvp in oidcConfig)
875 {
876 var configName = kvp.Key;
877 authBuilder
878 .AddOpenIdConnect(
879 GetOidcScheme(configName),
880 options =>
881 {
882 var config = kvp.Value;
883
884 options.Authority = config.Authority?.ToString();
885 options.ClientId = config.ClientId;
886 options.ClientSecret = config.ClientSecret;
887
888 options.Events = new OpenIdConnectEvents
889 {
890 OnRemoteFailure = context =>
891 {
892 context.HandleResponse();
893 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)}");
894 return Task.CompletedTask;
895 },
896 OnTicketReceived = context =>
897 {
898 var services = context
899 .HttpContext
900 .RequestServices;
901 var tokenFactory = services
902 .GetRequiredService<ITokenFactory>();
903 var authenticationContext = services
904 .GetRequiredService<IAuthenticationContext>();
905 context.HandleResponse();
906 context.HttpContext.Response.Redirect($"{config.ReturnPath}?code={HttpUtility.UrlEncode(tokenFactory.CreateToken(authenticationContext.User, true))}&state=oidc.{HttpUtility.UrlEncode(configName)}");
907 return Task.CompletedTask;
908 },
909 };
910
911 Task CompleteAuth(RemoteAuthenticationContext<OpenIdConnectOptions> context)
912 => context
913 .HttpContext
914 .RequestServices
915 .GetRequiredService<ITokenValidator>()
916 .ValidateOidcToken(
917 context,
918 configName,
919 config.GroupIdClaim,
920 context
921 .HttpContext
922 .RequestAborted);
923
924 if (securityConfiguration.OidcStrictMode)
925 {
926 options.GetClaimsFromUserInfoEndpoint = true;
927 options.ClaimActions.MapUniqueJsonKey(config.GroupIdClaim, config.GroupIdClaim);
928 options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
929 {
930 NameClaimType = config.UsernameClaim,
931 RoleClaimType = "roles",
932 };
933
934 options.Scope.Add(OpenIdConnectScope.Profile);
935 options.Events.OnUserInformationReceived = CompleteAuth;
936 }
937 else
938 options.Events.OnTokenValidated = CompleteAuth;
939
940 options.Scope.Add(OpenIdConnectScope.OpenId);
941 options.Scope.Add(OpenIdConnectScope.OfflineAccess);
942
943#if DEBUG
944 options.RequireHttpsMetadata = false;
945#endif
946
947 options.SaveTokens = true;
948 options.ResponseType = OpenIdConnectResponseType.Code;
949 options.MapInboundClaims = false;
950
951 options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
952
953 var basePath = $"/oidc/{configName}/";
954 options.CallbackPath = new PathString(basePath + "signin-callback");
955 options.SignedOutCallbackPath = new PathString(basePath + "signout-callback");
956 options.RemoteSignOutPath = new PathString(basePath + "signout");
957 });
958 }
959 }
960 }
961}
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.
Base Controller for API functions.
const ushort MaximumPageSize
Maximum size of Paginated<TModel> results.
const ushort DefaultPageSize
Default size of Paginated<TModel> results.
const string ControlPanelRoute
Route to the ControlPanelController.
Backend abstract implementation of IDatabaseContext.
IErrorFilter for transforming ErrorMessageResponse-like Exception.
GraphQL query global::System.Type.
Definition Query.cs:12
A ScalarType<TRuntimeType, TLiteral> for semantic Versions.
Definition SemverType.cs:13
Root type for GraphQL subscriptions.
IGateway for the SwarmNode this query is executing on.
IIOManager that resolves paths to Environment.CurrentDirectory.
IPostWriteHandler for POSIX systems.
An IIOManager that resolve relative paths from another IIOManager to a subdirectory of that.
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...
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 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.
Interface for using filesystems.
Definition IIOManager.cs:13
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.
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.
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.
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.