tgstation-server 6.12.3
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
533 public void Configure(
534 IApplicationBuilder applicationBuilder,
535 IServerControl serverControl,
537 IServerPortProvider serverPortProvider,
538 IAssemblyInformationProvider assemblyInformationProvider,
539 IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
540 IOptions<GeneralConfiguration> generalConfigurationOptions,
541 IOptions<SwarmConfiguration> swarmConfigurationOptions,
542 IOptions<InternalConfiguration> internalConfigurationOptions,
543 ILogger<Application> logger)
544 {
545 ArgumentNullException.ThrowIfNull(applicationBuilder);
546 ArgumentNullException.ThrowIfNull(serverControl);
547
548 this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
549
550 ArgumentNullException.ThrowIfNull(serverPortProvider);
551 ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
552
553 var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
554 var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
555 var swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
556 var internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions));
557
558 ArgumentNullException.ThrowIfNull(logger);
559
560 logger.LogDebug("Content Root: {contentRoot}", hostingEnvironment.ContentRootPath);
561 logger.LogTrace("Web Root: {webRoot}", hostingEnvironment.WebRootPath);
562
563 // setup the HTTP request pipeline
564 // Add additional logging context to the request
565 applicationBuilder.UseAdditionalRequestLoggingContext(swarmConfiguration);
566
567 // Wrap exceptions in a 500 (ErrorMessage) response
568 applicationBuilder.UseServerErrorHandling();
569
570 // Add the X-Powered-By response header
571 applicationBuilder.UseServerBranding(assemblyInformationProvider);
572
573 // Add the X-Accel-Buffering response header
574 applicationBuilder.UseDisabledNginxProxyBuffering();
575
576 // suppress OperationCancelledExceptions, they are just aborted HTTP requests
577 applicationBuilder.UseCancelledRequestSuppression();
578
579 // 503 requests made while the application is starting
580 applicationBuilder.UseAsyncInitialization<IInstanceManager>(
581 (instanceManager, cancellationToken) => instanceManager.Ready.WaitAsync(cancellationToken));
582
583 if (generalConfiguration.HostApiDocumentation)
584 {
585 var siteDocPath = Routes.ApiRoot + $"doc/{SwaggerConfiguration.DocumentName}.json";
586 if (!String.IsNullOrWhiteSpace(controlPanelConfiguration.PublicPath))
587 siteDocPath = controlPanelConfiguration.PublicPath.TrimEnd('/') + siteDocPath;
588
589 applicationBuilder.UseSwagger(options =>
590 {
591 options.RouteTemplate = Routes.ApiRoot + "doc/{documentName}.{json|yaml}";
592 });
593 applicationBuilder.UseSwaggerUI(options =>
594 {
596 options.SwaggerEndpoint(siteDocPath, "TGS API");
597 });
598 logger.LogTrace("Swagger API generation enabled");
599 }
600
601 // spa loading if necessary
602 if (controlPanelConfiguration.Enable)
603 {
604 logger.LogInformation("Web control panel enabled.");
605 applicationBuilder.UseFileServer(new FileServerOptions
606 {
608 EnableDefaultFiles = true,
609 EnableDirectoryBrowsing = false,
610 RedirectToAppendTrailingSlash = false,
611 });
612 }
613 else
614#if NO_WEBPANEL
615 logger.LogDebug("Web control panel was not included in TGS build!");
616#else
617 logger.LogTrace("Web control panel disabled!");
618#endif
619
620 // Enable endpoint routing
621 applicationBuilder.UseRouting();
622
623 // Set up CORS based on configuration if necessary
624 Action<CorsPolicyBuilder>? corsBuilder = null;
625 if (controlPanelConfiguration.AllowAnyOrigin)
626 {
627 logger.LogTrace("Access-Control-Allow-Origin: *");
628 corsBuilder = builder => builder.SetIsOriginAllowed(_ => true);
629 }
630 else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
631 {
632 logger.LogTrace("Access-Control-Allow-Origin: {allowedOrigins}", String.Join(',', controlPanelConfiguration.AllowedOrigins));
633 corsBuilder = builder => builder.WithOrigins([.. controlPanelConfiguration.AllowedOrigins]);
634 }
635
636 var originalBuilder = corsBuilder;
637 corsBuilder = builder =>
638 {
639 builder
640 .AllowAnyHeader()
641 .AllowAnyMethod()
642 .AllowCredentials()
643 .SetPreflightMaxAge(TimeSpan.FromDays(1));
644 originalBuilder?.Invoke(builder);
645 };
646 applicationBuilder.UseCors(corsBuilder);
647
648 // validate the API version
649 applicationBuilder.UseApiCompatibility();
650
651 // authenticate JWT tokens using our security pipeline if present, returns 401 if bad
652 applicationBuilder.UseAuthentication();
653
654 // enable authorization on endpoints
655 applicationBuilder.UseAuthorization();
656
657 // suppress and log database exceptions
658 applicationBuilder.UseDbConflictHandling();
659
660 // setup endpoints
661 applicationBuilder.UseEndpoints(endpoints =>
662 {
663 // access to the signalR jobs hub
664 endpoints.MapHub<JobsHub>(
666 options =>
667 {
668 options.Transports = HttpTransportType.ServerSentEvents;
669 options.CloseOnAuthenticationExpiration = true;
670 })
671 .RequireAuthorization()
672 .RequireCors(corsBuilder);
673
674 // majority of handling is done in the controllers
675 endpoints.MapControllers();
676
677 if (internalConfiguration.EnableGraphQL)
678 {
679 logger.LogWarning("Enabling GraphQL. This API is experimental and breaking changes may occur at any time!");
680 var gqlOptions = new GraphQLServerOptions
681 {
682 EnableBatching = true,
683 };
684
685 gqlOptions.Tool.Enable = generalConfiguration.HostApiDocumentation;
686
687 endpoints
688 .MapGraphQL(Routes.GraphQL)
689 .WithOptions(gqlOptions);
690 }
691 });
692
693 // 404 anything that gets this far
694 // End of request pipeline setup
695 logger.LogTrace("Configuration version: {configVersion}", GeneralConfiguration.CurrentConfigVersion);
696 logger.LogTrace("DMAPI Interop version: {interopVersion}", DMApiConstants.InteropVersion);
697 if (controlPanelConfiguration.Enable)
698 logger.LogTrace("Webpanel version: {webCPVersion}", MasterVersionsAttribute.Instance.RawWebpanelVersion);
699
700 logger.LogDebug("Starting hosting on port {httpApiPort}...", serverPortProvider.HttpApiPort);
701 }
702
704 protected override void ConfigureHostedService(IServiceCollection services)
705 => services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
706
711 void ConfigureAuthenticationPipeline(IServiceCollection services)
712 {
713 services.AddHttpContextAccessor();
714 services.AddScoped<IApiHeadersProvider, ApiHeadersProvider>();
715 services.AddScoped<AuthenticationContextFactory>();
716 services.AddScoped<ITokenValidator>(provider => provider.GetRequiredService<AuthenticationContextFactory>());
717
718 // what if you
719 // wanted to just do this:
720 // return provider.GetRequiredService<AuthenticationContextFactory>().CurrentAuthenticationContext
721 // But M$ said
722 // https://stackoverflow.com/questions/56792917/scoped-services-in-asp-net-core-with-signalr-hubs
723 services.AddScoped(provider => (provider
724 .GetRequiredService<IHttpContextAccessor>()
725 .HttpContext ?? throw new InvalidOperationException($"Unable to resolve {nameof(IAuthenticationContext)} due to no HttpContext being available!"))
726 .RequestServices
727 .GetRequiredService<AuthenticationContextFactory>()
728 .CurrentAuthenticationContext);
730
731 services
732 .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
733 .AddJwtBearer(jwtBearerOptions =>
734 {
735 // this line isn't actually run until the first request is made
736 // at that point tokenFactory will be populated
737 jwtBearerOptions.TokenValidationParameters = tokenFactory?.ValidationParameters ?? throw new InvalidOperationException("tokenFactory not initialized!");
738 jwtBearerOptions.MapInboundClaims = false;
739 jwtBearerOptions.Events = new JwtBearerEvents
740 {
741 OnMessageReceived = context =>
742 {
743 if (String.IsNullOrWhiteSpace(context.Token))
744 {
745 var accessToken = context.Request.Query["access_token"];
746 var path = context.HttpContext.Request.Path;
747
748 if (!String.IsNullOrWhiteSpace(accessToken) &&
749 path.StartsWithSegments(Routes.HubsRoot, StringComparison.OrdinalIgnoreCase))
750 {
751 context.Token = accessToken;
752 }
753 }
754
755 return Task.CompletedTask;
756 },
757 OnTokenValidated = context => context
758 .HttpContext
759 .RequestServices
760 .GetRequiredService<ITokenValidator>()
761 .ValidateToken(
762 context,
763 context
764 .HttpContext
765 .RequestAborted),
766 };
767 });
768 }
769 }
770}
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< 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.