tgstation-server 6.14.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;
6
7using Cyberboss.AspNetCore.AsyncInitializer;
8
9using Elastic.CommonSchema.Serilog;
10
11using HotChocolate.AspNetCore;
12using HotChocolate.Subscriptions;
13using HotChocolate.Types;
14
15using Microsoft.AspNetCore.Authentication;
16using Microsoft.AspNetCore.Authentication.JwtBearer;
17using Microsoft.AspNetCore.Builder;
18using Microsoft.AspNetCore.Cors.Infrastructure;
19using Microsoft.AspNetCore.Hosting;
20using Microsoft.AspNetCore.Http;
21using Microsoft.AspNetCore.Http.Connections;
22using Microsoft.AspNetCore.Identity;
23using Microsoft.AspNetCore.Mvc.Infrastructure;
24using Microsoft.AspNetCore.SignalR;
25using Microsoft.Extensions.Configuration;
26using Microsoft.Extensions.DependencyInjection;
27using Microsoft.Extensions.Hosting;
28using Microsoft.Extensions.Logging;
29using Microsoft.Extensions.Options;
30
31using Newtonsoft.Json;
32
33using Prometheus;
34
35using Serilog;
36using Serilog.Events;
37using Serilog.Formatting.Display;
38using Serilog.Sinks.Elasticsearch;
39
77
79{
83#pragma warning disable CA1506
84 public sealed class Application : SetupApplication
85 {
89 readonly IWebHostEnvironment hostingEnvironment;
90
95
101 {
102 var assemblyInformationProvider = new AssemblyInformationProvider();
103 var ioManager = new DefaultIOManager();
104 return new ServerFactory(
105 assemblyInformationProvider,
106 ioManager);
107 }
108
115 static void AddWatchdog<TSystemWatchdogFactory>(IServiceCollection services, IPostSetupServices postSetupServices)
116 where TSystemWatchdogFactory : class, IWatchdogFactory
117 {
118 if (postSetupServices.GeneralConfiguration.UseBasicWatchdog)
119 services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
120 else
121 services.AddSingleton<IWatchdogFactory, TSystemWatchdogFactory>();
122 }
123
130 IConfiguration configuration,
131 IWebHostEnvironment hostingEnvironment)
132 : base(configuration)
133 {
134 this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
135 }
136
144 public void ConfigureServices(
145 IServiceCollection services,
146 IAssemblyInformationProvider assemblyInformationProvider,
147 IIOManager ioManager,
148 IPostSetupServices postSetupServices)
149 {
150 ConfigureServices(services, assemblyInformationProvider, ioManager);
151
152 ArgumentNullException.ThrowIfNull(postSetupServices);
153
154 // configure configuration
155 services.UseStandardConfig<UpdatesConfiguration>(Configuration);
156 services.UseStandardConfig<ControlPanelConfiguration>(Configuration);
157 services.UseStandardConfig<SwarmConfiguration>(Configuration);
158 services.UseStandardConfig<SessionConfiguration>(Configuration);
159 services.UseStandardConfig<TelemetryConfiguration>(Configuration);
160
161 // enable options which give us config reloading
162 services.AddOptions();
163
164 // Set the timeout for IHostedService.StopAsync
165 services.Configure<HostOptions>(
166 opts => opts.ShutdownTimeout = TimeSpan.FromMinutes(postSetupServices.GeneralConfiguration.RestartTimeoutMinutes));
167
168 static LogEventLevel? ConvertSeriLogLevel(LogLevel logLevel) =>
169 logLevel switch
170 {
171 LogLevel.Critical => LogEventLevel.Fatal,
172 LogLevel.Debug => LogEventLevel.Debug,
173 LogLevel.Error => LogEventLevel.Error,
174 LogLevel.Information => LogEventLevel.Information,
175 LogLevel.Trace => LogEventLevel.Verbose,
176 LogLevel.Warning => LogEventLevel.Warning,
177 LogLevel.None => null,
178 _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel)),
179 };
180
181 var microsoftEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.MicrosoftLogLevel);
182 var elasticsearchConfiguration = postSetupServices.ElasticsearchConfiguration;
183 services.SetupLogging(
184 config =>
185 {
186 if (microsoftEventLevel.HasValue)
187 {
188 config.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
189 config.MinimumLevel.Override("System.Net.Http.HttpClient", microsoftEventLevel.Value);
190 }
191 },
192 sinkConfig =>
193 {
194 if (postSetupServices.FileLoggingConfiguration.Disable)
195 return;
196
197 var logPath = postSetupServices.FileLoggingConfiguration.GetFullLogDirectory(
198 ioManager,
199 assemblyInformationProvider,
200 postSetupServices.PlatformIdentifier);
201
202 var logEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.LogLevel);
203
204 var formatter = new MessageTemplateTextFormatter(
205 "{Timestamp:o} "
207 + "): [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}",
208 null);
209
210 logPath = ioManager.ConcatPath(logPath, "tgs-.log");
211 var rollingFileConfig = sinkConfig.File(
212 formatter,
213 logPath,
214 logEventLevel ?? LogEventLevel.Verbose,
215 50 * 1024 * 1024, // 50MB max size
216 flushToDiskInterval: TimeSpan.FromSeconds(2),
217 rollingInterval: RollingInterval.Day,
218 rollOnFileSizeLimit: true);
219 },
220 elasticsearchConfiguration.Enable
221 ? new ElasticsearchSinkOptions(elasticsearchConfiguration.Host ?? throw new InvalidOperationException($"Missing {ElasticsearchConfiguration.Section}:{nameof(elasticsearchConfiguration.Host)}!"))
222 {
223 // Yes I know this means they cannot use a self signed cert unless they also have authentication, but lets be real here
224 // No one is going to be doing one of those but not the other
225 ModifyConnectionSettings = connectionConfigration => (!String.IsNullOrWhiteSpace(elasticsearchConfiguration.Username) && !String.IsNullOrWhiteSpace(elasticsearchConfiguration.Password))
226 ? connectionConfigration
227 .BasicAuthentication(
228 elasticsearchConfiguration.Username,
229 elasticsearchConfiguration.Password)
230 .ServerCertificateValidationCallback((o, certificate, chain, errors) => true)
231 : null,
232 CustomFormatter = new EcsTextFormatter(),
233 AutoRegisterTemplate = true,
234 AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7,
235 IndexFormat = "tgs-logs",
236 }
237 : null,
238 postSetupServices.InternalConfiguration,
239 postSetupServices.FileLoggingConfiguration);
240
241 // configure authentication pipeline
243
244 // add mvc, configure the json serializer settings
245 var jsonVersionConverterList = new List<JsonConverter>
246 {
247 new VersionConverter(),
248 };
249
250 void ConfigureNewtonsoftJsonSerializerSettingsForApi(JsonSerializerSettings settings)
251 {
252 settings.NullValueHandling = NullValueHandling.Ignore;
253 settings.CheckAdditionalContent = true;
254 settings.MissingMemberHandling = MissingMemberHandling.Error;
255 settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
256 settings.Converters = jsonVersionConverterList;
257 }
258
259 services
260 .AddMvc(options =>
261 {
262 options.ReturnHttpNotAcceptable = true;
263 options.RespectBrowserAcceptHeader = true;
264 })
265 .AddNewtonsoftJson(options =>
266 {
267 options.AllowInputFormatterExceptionMessages = true;
268 ConfigureNewtonsoftJsonSerializerSettingsForApi(options.SerializerSettings);
269 });
270
271 services.AddSignalR(
272 options =>
273 {
274 options.AddFilter<AuthorizationContextHubFilter>();
275 })
276 .AddNewtonsoftJsonProtocol(options =>
277 {
278 ConfigureNewtonsoftJsonSerializerSettingsForApi(options.PayloadSerializerSettings);
279 });
280
281 services.AddHub<JobsHub, IJobsHub>();
282
283 if (postSetupServices.GeneralConfiguration.HostApiDocumentation)
284 {
285 string GetDocumentationFilePath(string assemblyLocation) => ioManager.ConcatPath(ioManager.GetDirectoryName(assemblyLocation), String.Concat(ioManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
286 var assemblyDocumentationPath = GetDocumentationFilePath(GetType().Assembly.Location);
287 var apiDocumentationPath = GetDocumentationFilePath(typeof(ApiHeaders).Assembly.Location);
288 services.AddSwaggerGen(genOptions => SwaggerConfiguration.Configure(genOptions, assemblyDocumentationPath, apiDocumentationPath));
289 services.AddSwaggerGenNewtonsoftSupport();
290 }
291
292 // CORS conditionally enabled later
293 services.AddCors();
294
295 // Enable managed HTTP clients
296 services.AddHttpClient();
298
299 // configure metrics
300 var prometheusPort = postSetupServices.GeneralConfiguration.PrometheusPort;
301
302 services.AddSingleton<IMetricFactory>(_ => Metrics.DefaultFactory);
303 services.AddSingleton<ICollectorRegistry>(_ => Metrics.DefaultRegistry);
304
305 if (prometheusPort.HasValue && prometheusPort != postSetupServices.GeneralConfiguration.ApiPort)
306 services.AddMetricServer(options => options.Port = prometheusPort.Value);
307
308 services.UseHttpClientMetrics();
309
310 var healthChecksBuilder = services
311 .AddHealthChecks()
312 .ForwardToPrometheus();
313
314 // configure graphql
315 services
316 .AddScoped<GraphQL.Subscriptions.ITopicEventReceiver, ShutdownAwareTopicEventReceiver>()
317 .AddGraphQLServer()
318 .AddAuthorization(
319 options => options.AddPolicy(
321 builder => builder.RequireRole(TgsAuthorizeAttribute.UserEnabledRole)))
322 .ModifyOptions(options =>
323 {
324 options.EnsureAllNodesCanBeResolved = true;
325 options.EnableFlagEnums = true;
326 })
327#if DEBUG
328 .ModifyCostOptions(options =>
329 {
330 options.EnforceCostLimits = false;
331 })
332#endif
333 .AddMutationConventions()
334 .AddInMemorySubscriptions(
335 new SubscriptionOptions
336 {
337 TopicBufferCapacity = 1024, // mainly so high for tests, not possible to DoS the server without authentication and some other access to generate messages
338 })
339 .AddGlobalObjectIdentification()
340 .AddQueryFieldToMutationPayloads()
341 .ModifyOptions(options =>
342 {
343 options.EnableDefer = true;
344 })
345 .ModifyPagingOptions(pagingOptions =>
346 {
347 pagingOptions.IncludeTotalCount = true;
348 pagingOptions.RequirePagingBoundaries = false;
349 pagingOptions.DefaultPageSize = ApiController.DefaultPageSize;
350 pagingOptions.MaxPageSize = ApiController.MaximumPageSize;
351 })
352 .AddFiltering()
353 .AddSorting()
354 .AddHostTypes()
355 .AddErrorFilter<ErrorMessageFilter>()
356 .AddType<StandaloneNode>()
357 .AddType<LocalGateway>()
358 .AddType<RemoteGateway>()
359 .AddType<GraphQL.Types.UserName>()
360 .AddType<UnsignedIntType>()
361 .BindRuntimeType<Version, SemverType>()
362 .TryAddTypeInterceptor<RightsTypeInterceptor>()
363 .AddQueryType<Query>()
364 .AddMutationType<Mutation>()
365 .AddSubscriptionType<Subscription>();
366
367 void AddTypedContext<TContext>()
368 where TContext : DatabaseContext
369 {
370 var configureAction = DatabaseContext.GetConfigureAction<TContext>();
371
372 services.AddDbContextPool<TContext>((serviceProvider, builder) =>
373 {
374 if (hostingEnvironment.IsDevelopment())
375 builder.EnableSensitiveDataLogging();
376
377 var databaseConfigOptions = serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>();
378 var databaseConfig = databaseConfigOptions.Value ?? throw new InvalidOperationException("DatabaseConfiguration missing!");
379 configureAction(builder, databaseConfig);
380 });
381 services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
382
383 healthChecksBuilder
384 .AddDbContextCheck<TContext>();
385 }
386
387 // add the correct database context type
388 var dbType = postSetupServices.DatabaseConfiguration.DatabaseType;
389 switch (dbType)
390 {
391 case DatabaseType.MySql:
392 case DatabaseType.MariaDB:
393 AddTypedContext<MySqlDatabaseContext>();
394 break;
395 case DatabaseType.SqlServer:
396 AddTypedContext<SqlServerDatabaseContext>();
397 break;
398 case DatabaseType.Sqlite:
399 AddTypedContext<SqliteDatabaseContext>();
400 break;
401 case DatabaseType.PostgresSql:
402 AddTypedContext<PostgresSqlDatabaseContext>();
403 break;
404 default:
405 throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
406 }
407
408 // configure other database services
409 services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
410 services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
411
412 // configure other security services
413 services.AddSingleton<IOAuthProviders, OAuthProviders>();
414 services.AddSingleton<IIdentityCache, IdentityCache>();
415 services.AddSingleton<ICryptographySuite, CryptographySuite>();
416 services.AddSingleton<ITokenFactory, TokenFactory>();
418 services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
419
420 // configure platform specific services
421 if (postSetupServices.PlatformIdentifier.IsWindows)
422 {
423 AddWatchdog<WindowsWatchdogFactory>(services, postSetupServices);
426 services.AddSingleton<ByondInstallerBase, WindowsByondInstaller>();
427 services.AddSingleton<OpenDreamInstaller, WindowsOpenDreamInstaller>();
428 services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
429 services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
430
431 services.AddSingleton<WindowsNetworkPromptReaper>();
432 services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
433 services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
434 }
435 else
436 {
437 AddWatchdog<PosixWatchdogFactory>(services, postSetupServices);
438 services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
439 services.AddSingleton<IFilesystemLinkFactory, PosixFilesystemLinkFactory>();
440 services.AddSingleton<ByondInstallerBase, PosixByondInstaller>();
441 services.AddSingleton<OpenDreamInstaller>();
442 services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
443
444 services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
445 services.AddHostedService<PosixProcessFeatures>();
446
447 // PosixProcessFeatures also needs a IProcessExecutor for gcore
448 services.AddSingleton(x => new Lazy<IProcessExecutor>(() => x.GetRequiredService<IProcessExecutor>(), true));
449 services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
450
451 services.AddHostedService<PosixSignalHandler>();
452 }
453
454 // only global repo manager should be for the OD repo
455 // god help me if we need more
456 var openDreamRepositoryDirectory = ioManager.ConcatPath(
457 ioManager.GetPathInLocalDirectory(assemblyInformationProvider),
458 "OpenDreamRepository");
459 services.AddSingleton(
460 services => services
461 .GetRequiredService<IRepositoryManagerFactory>()
462 .CreateRepositoryManager(
464 services.GetRequiredService<IIOManager>(),
465 openDreamRepositoryDirectory),
466 new NoopEventConsumer()));
467
468 services.AddSingleton(
469 serviceProvider => new Dictionary<EngineType, IEngineInstaller>
470 {
471 { EngineType.Byond, serviceProvider.GetRequiredService<ByondInstallerBase>() },
472 { EngineType.OpenDream, serviceProvider.GetRequiredService<OpenDreamInstaller>() },
473 }
474 .ToFrozenDictionary());
475 services.AddSingleton<IEngineInstaller, DelegatingEngineInstaller>();
476
477 if (postSetupServices.InternalConfiguration.UsingSystemD)
478 services.AddHostedService<SystemDManager>();
479
480 // configure file transfer services
481 services.AddSingleton<FileTransferService>();
482 services.AddSingleton<IFileTransferStreamHandler>(x => x.GetRequiredService<FileTransferService>());
483 services.AddSingleton<IFileTransferTicketProvider>(x => x.GetRequiredService<FileTransferService>());
485
486 // configure swarm service
487 services.AddSingleton<SwarmService>();
488 services.AddSingleton<ISwarmService>(x => x.GetRequiredService<SwarmService>());
489 services.AddSingleton<ISwarmOperations>(x => x.GetRequiredService<SwarmService>());
490 services.AddSingleton<ISwarmServiceController>(x => x.GetRequiredService<SwarmService>());
491
492 // configure component services
493 services.AddSingleton<IPortAllocator, PortAllocator>();
494 services.AddSingleton<IInstanceFactory, InstanceFactory>();
495 services.AddSingleton<IGitRemoteFeaturesFactory, GitRemoteFeaturesFactory>();
496 services.AddSingleton<ILibGit2RepositoryFactory, LibGit2RepositoryFactory>();
497 services.AddSingleton<ILibGit2Commands, LibGit2Commands>();
498 services.AddSingleton<IRepositoryManagerFactory, RepostoryManagerFactory>();
500 services.AddChatProviderFactory();
501 services.AddSingleton<IChatManagerFactory, ChatManagerFactory>();
502 services.AddSingleton<IServerUpdater, ServerUpdater>();
503 services.AddSingleton<IServerUpdateInitiator, ServerUpdateInitiator>();
504 services.AddSingleton<IDotnetDumpService, DotnetDumpService>();
505
506 // configure authorities
507 services.AddScoped(typeof(IRestAuthorityInvoker<>), typeof(RestAuthorityInvoker<>));
508 services.AddScoped(typeof(IGraphQLAuthorityInvoker<>), typeof(GraphQLAuthorityInvoker<>));
509 services.AddScoped<ILoginAuthority, LoginAuthority>();
510 services.AddScoped<IUserAuthority, UserAuthority>();
511 services.AddScoped<IUserGroupAuthority, UserGroupAuthority>();
512 services.AddScoped<IPermissionSetAuthority, PermissionSetAuthority>();
514
515 // configure misc services
516 services.AddSingleton<IProcessExecutor, ProcessExecutor>();
517 services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
518 services.AddSingleton<IServerPortProvider, ServerPortProivder>();
519 services.AddSingleton<ITopicClientFactory, TopicClientFactory>();
520 services.AddHostedService<CommandPipeManager>();
521 services.AddHostedService<VersionReportingService>();
522
523 services.AddFileDownloader();
524 services.AddGitHub();
525
526 // configure root services
527 services.AddSingleton<JobService>();
528 services.AddSingleton<IJobService>(provider => provider.GetRequiredService<JobService>());
529 services.AddSingleton<IJobsHubUpdater>(provider => provider.GetRequiredService<JobService>());
530 services.AddSingleton<IJobManager>(x => x.GetRequiredService<IJobService>());
531 services.AddSingleton<JobsHubGroupMapper>();
532 services.AddSingleton<IPermissionsUpdateNotifyee>(provider => provider.GetRequiredService<JobsHubGroupMapper>());
533 services.AddSingleton<IHostedService>(x => x.GetRequiredService<JobsHubGroupMapper>()); // bit of a hack, but we need this to load immediated
534
535 services.AddSingleton<InstanceManager>();
536 services.AddSingleton<IBridgeDispatcher>(x => x.GetRequiredService<InstanceManager>());
537 services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
538 }
539
554 public void Configure(
555 IApplicationBuilder applicationBuilder,
556 IServerControl serverControl,
558 IServerPortProvider serverPortProvider,
559 IAssemblyInformationProvider assemblyInformationProvider,
560 IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
561 IOptions<GeneralConfiguration> generalConfigurationOptions,
562 IOptions<DatabaseConfiguration> databaseConfigurationOptions,
563 IOptions<SwarmConfiguration> swarmConfigurationOptions,
564 IOptions<InternalConfiguration> internalConfigurationOptions,
565 ILogger<Application> logger)
566 {
567 ArgumentNullException.ThrowIfNull(applicationBuilder);
568 ArgumentNullException.ThrowIfNull(serverControl);
569
570 this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
571
572 ArgumentNullException.ThrowIfNull(serverPortProvider);
573 ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
574
575 var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
576 var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
577 var databaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
578 var swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
579 var internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions));
580
581 ArgumentNullException.ThrowIfNull(logger);
582
583 logger.LogDebug("Database provider: {provider}", databaseConfiguration.DatabaseType);
584 logger.LogDebug("Content Root: {contentRoot}", hostingEnvironment.ContentRootPath);
585 logger.LogTrace("Web Root: {webRoot}", hostingEnvironment.WebRootPath);
586
587 // setup the HTTP request pipeline
588 // Add additional logging context to the request
589 applicationBuilder.UseAdditionalRequestLoggingContext(swarmConfiguration);
590
591 // Wrap exceptions in a 500 (ErrorMessage) response
592 applicationBuilder.UseServerErrorHandling();
593
594 // metrics capture
595 applicationBuilder.UseHttpMetrics();
596
597 // Add the X-Powered-By response header
598 applicationBuilder.UseServerBranding(assemblyInformationProvider);
599
600 // Add the X-Accel-Buffering response header
601 applicationBuilder.UseDisabledNginxProxyBuffering();
602
603 // suppress OperationCancelledExceptions, they are just aborted HTTP requests
604 applicationBuilder.UseCancelledRequestSuppression();
605
606 // 503 requests made while the application is starting
607 applicationBuilder.UseAsyncInitialization<IInstanceManager>(
608 (instanceManager, cancellationToken) => instanceManager.Ready.WaitAsync(cancellationToken));
609
610 if (generalConfiguration.HostApiDocumentation)
611 {
612 var siteDocPath = Routes.ApiRoot + $"doc/{SwaggerConfiguration.DocumentName}.json";
613 if (!String.IsNullOrWhiteSpace(controlPanelConfiguration.PublicPath))
614 siteDocPath = controlPanelConfiguration.PublicPath.TrimEnd('/') + siteDocPath;
615
616 applicationBuilder.UseSwagger(options =>
617 {
618 options.RouteTemplate = Routes.ApiRoot + "doc/{documentName}.{json|yaml}";
619 });
620 applicationBuilder.UseSwaggerUI(options =>
621 {
623 options.SwaggerEndpoint(siteDocPath, "TGS API");
624 });
625 logger.LogTrace("Swagger API generation enabled");
626 }
627
628 // spa loading if necessary
629 if (controlPanelConfiguration.Enable)
630 {
631 logger.LogInformation("Web control panel enabled.");
632 applicationBuilder.UseFileServer(new FileServerOptions
633 {
635 EnableDefaultFiles = true,
636 EnableDirectoryBrowsing = false,
637 RedirectToAppendTrailingSlash = false,
638 });
639 }
640 else
641#if NO_WEBPANEL
642 logger.LogDebug("Web control panel was not included in TGS build!");
643#else
644 logger.LogTrace("Web control panel disabled!");
645#endif
646
647 // Enable endpoint routing
648 applicationBuilder.UseRouting();
649
650 // Set up CORS based on configuration if necessary
651 Action<CorsPolicyBuilder>? corsBuilder = null;
652 if (controlPanelConfiguration.AllowAnyOrigin)
653 {
654 logger.LogTrace("Access-Control-Allow-Origin: *");
655 corsBuilder = builder => builder.SetIsOriginAllowed(_ => true);
656 }
657 else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
658 {
659 logger.LogTrace("Access-Control-Allow-Origin: {allowedOrigins}", String.Join(',', controlPanelConfiguration.AllowedOrigins));
660 corsBuilder = builder => builder.WithOrigins([.. controlPanelConfiguration.AllowedOrigins]);
661 }
662
663 var originalBuilder = corsBuilder;
664 corsBuilder = builder =>
665 {
666 builder
667 .AllowAnyHeader()
668 .AllowAnyMethod()
669 .AllowCredentials()
670 .SetPreflightMaxAge(TimeSpan.FromDays(1));
671 originalBuilder?.Invoke(builder);
672 };
673 applicationBuilder.UseCors(corsBuilder);
674
675 // validate the API version
676 applicationBuilder.UseApiCompatibility();
677
678 // authenticate JWT tokens using our security pipeline if present, returns 401 if bad
679 applicationBuilder.UseAuthentication();
680
681 // enable authorization on endpoints
682 applicationBuilder.UseAuthorization();
683
684 // suppress and log database exceptions
685 applicationBuilder.UseDbConflictHandling();
686
687 // setup endpoints
688 applicationBuilder.UseEndpoints(endpoints =>
689 {
690 // access to the signalR jobs hub
691 endpoints.MapHub<JobsHub>(
693 options =>
694 {
695 options.Transports = HttpTransportType.ServerSentEvents;
696 options.CloseOnAuthenticationExpiration = true;
697 })
698 .RequireAuthorization()
699 .RequireCors(corsBuilder);
700
701 // majority of handling is done in the controllers
702 endpoints.MapControllers();
703
704 if (internalConfiguration.EnableGraphQL)
705 {
706 logger.LogWarning("Enabling GraphQL. This API is experimental and breaking changes may occur at any time!");
707 var gqlOptions = new GraphQLServerOptions
708 {
709 EnableBatching = true,
710 };
711
712 gqlOptions.Tool.Enable = generalConfiguration.HostApiDocumentation;
713
714 endpoints
715 .MapGraphQL(Routes.GraphQL)
716 .WithOptions(gqlOptions);
717 }
718
719 if (generalConfiguration.PrometheusPort.HasValue)
720 if (generalConfiguration.PrometheusPort == generalConfiguration.ApiPort)
721 {
722 endpoints.MapMetrics();
723 logger.LogDebug("Prometheus being hosted alongside server");
724 }
725 else
726 logger.LogDebug("Prometheus being hosted on port {prometheusPort}", generalConfiguration.PrometheusPort);
727 else
728 logger.LogTrace("Prometheus disabled");
729
730 endpoints.MapHealthChecks("/health");
731 });
732
733 // 404 anything that gets this far
734 // End of request pipeline setup
735 logger.LogTrace("Configuration version: {configVersion}", GeneralConfiguration.CurrentConfigVersion);
736 logger.LogTrace("DMAPI Interop version: {interopVersion}", DMApiConstants.InteropVersion);
737 if (controlPanelConfiguration.Enable)
738 logger.LogTrace("Webpanel version: {webCPVersion}", MasterVersionsAttribute.Instance.RawWebpanelVersion);
739
740 logger.LogDebug("Starting hosting on port {httpApiPort}...", serverPortProvider.HttpApiPort);
741 }
742
744 protected override void ConfigureHostedService(IServiceCollection services)
745 => services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
746
751 void ConfigureAuthenticationPipeline(IServiceCollection services)
752 {
753 services.AddHttpContextAccessor();
754 services.AddScoped<IApiHeadersProvider, ApiHeadersProvider>();
755 services.AddScoped<AuthenticationContextFactory>();
756 services.AddScoped<ITokenValidator>(provider => provider.GetRequiredService<AuthenticationContextFactory>());
757
758 // what if you
759 // wanted to just do this:
760 // return provider.GetRequiredService<AuthenticationContextFactory>().CurrentAuthenticationContext
761 // But M$ said
762 // https://stackoverflow.com/questions/56792917/scoped-services-in-asp-net-core-with-signalr-hubs
763 services.AddScoped(provider => (provider
764 .GetRequiredService<IHttpContextAccessor>()
765 .HttpContext ?? throw new InvalidOperationException($"Unable to resolve {nameof(IAuthenticationContext)} due to no HttpContext being available!"))
766 .RequestServices
767 .GetRequiredService<AuthenticationContextFactory>()
768 .CurrentAuthenticationContext);
770
771 services
772 .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
773 .AddJwtBearer(jwtBearerOptions =>
774 {
775 // this line isn't actually run until the first request is made
776 // at that point tokenFactory will be populated
777 jwtBearerOptions.TokenValidationParameters = tokenFactory?.ValidationParameters ?? throw new InvalidOperationException("tokenFactory not initialized!");
778 jwtBearerOptions.MapInboundClaims = false;
779 jwtBearerOptions.Events = new JwtBearerEvents
780 {
781 OnMessageReceived = context =>
782 {
783 if (String.IsNullOrWhiteSpace(context.Token))
784 {
785 var accessToken = context.Request.Query["access_token"];
786 var path = context.HttpContext.Request.Path;
787
788 if (!String.IsNullOrWhiteSpace(accessToken) &&
789 path.StartsWithSegments(Routes.HubsRoot, StringComparison.OrdinalIgnoreCase))
790 {
791 context.Token = accessToken;
792 }
793 }
794
795 return Task.CompletedTask;
796 },
797 OnTokenValidated = context => context
798 .HttpContext
799 .RequestServices
800 .GetRequiredService<ITokenValidator>()
801 .ValidateToken(
802 context,
803 context
804 .HttpContext
805 .RequestAborted),
806 };
807 });
808 }
809 }
810}
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.
bool UsingSystemD
If the server is running under SystemD.
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.
Sets up dependency injection.
override void ConfigureHostedService(IServiceCollection services)
Configures the IHostedService.
static void AddWatchdog< TSystemWatchdogFactory >(IServiceCollection services, IPostSetupServices postSetupServices)
Adds the IWatchdogFactory implementation.
void ConfigureServices(IServiceCollection services, IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager, IPostSetupServices postSetupServices)
Configure the Application's services .
static IServerFactory CreateDefaultServerFactory()
Create the default IServerFactory.
void ConfigureAuthenticationPipeline(IServiceCollection services)
Configure the services for the authentication pipeline.
ITokenFactory? tokenFactory
The ITokenFactory for the Application.
Application(IConfiguration configuration, IWebHostEnvironment hostingEnvironment)
Initializes a new instance of the Application class.
void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IServerPortProvider serverPortProvider, IAssemblyInformationProvider assemblyInformationProvider, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, IOptions< GeneralConfiguration > generalConfigurationOptions, IOptions< DatabaseConfiguration > databaseConfigurationOptions, IOptions< SwarmConfiguration > swarmConfigurationOptions, IOptions< InternalConfiguration > internalConfigurationOptions, ILogger< Application > logger)
Configure the Application.
readonly IWebHostEnvironment hostingEnvironment
The IWebHostEnvironment for the Application.
Reads from the command pipe opened by the host watchdog.
Handles TGS version reporting, if enabled.
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.
IConfiguration Configuration
The IConfiguration for the SetupApplication.
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.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
Provides access to the server's HttpApiPort.
ushort HttpApiPort
The port the server listens on.
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.
Contains various cryptographic functions.
Receives notifications about permissions updates.
TokenValidationParameters ValidationParameters
The TokenValidationParameters for the ITokenFactory.
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.
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
DatabaseType
Type of database to user.