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