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