tgstation-server 6.11.1
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 .ModifyOptions(options =>
303 {
304 options.EnsureAllNodesCanBeResolved = true;
305 options.EnableFlagEnums = true;
306 })
307#if DEBUG
308 .ModifyCostOptions(options =>
309 {
310 options.EnforceCostLimits = false;
311 })
312#endif
313 .AddMutationConventions()
314 .AddInMemorySubscriptions(
315 new SubscriptionOptions
316 {
317 TopicBufferCapacity = 1024, // mainly so high for tests, not possible to DoS the server without authentication and some other access to generate messages
318 })
319 .AddGlobalObjectIdentification()
320 .AddQueryFieldToMutationPayloads()
321 .ModifyOptions(options =>
322 {
323 options.EnableDefer = true;
324 })
325 .ModifyPagingOptions(pagingOptions =>
326 {
327 pagingOptions.IncludeTotalCount = true;
328 pagingOptions.RequirePagingBoundaries = false;
329 pagingOptions.DefaultPageSize = ApiController.DefaultPageSize;
330 pagingOptions.MaxPageSize = ApiController.MaximumPageSize;
331 })
332 .AddFiltering()
333 .AddSorting()
334 .AddHostTypes()
335 .AddErrorFilter<ErrorMessageFilter>()
336 .AddType<StandaloneNode>()
337 .AddType<LocalGateway>()
338 .AddType<RemoteGateway>()
339 .AddType<GraphQL.Types.UserName>()
340 .AddType<UnsignedIntType>()
341 .BindRuntimeType<Version, SemverType>()
342 .TryAddTypeInterceptor<RightsTypeInterceptor>()
343 .AddQueryType<Query>()
344 .AddMutationType<Mutation>()
345 .AddSubscriptionType<Subscription>();
346
347 void AddTypedContext<TContext>()
348 where TContext : DatabaseContext
349 {
350 var configureAction = DatabaseContext.GetConfigureAction<TContext>();
351
352 services.AddDbContextPool<TContext>((serviceProvider, builder) =>
353 {
354 if (hostingEnvironment.IsDevelopment())
355 builder.EnableSensitiveDataLogging();
356
357 var databaseConfigOptions = serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>();
358 var databaseConfig = databaseConfigOptions.Value ?? throw new InvalidOperationException("DatabaseConfiguration missing!");
359 configureAction(builder, databaseConfig);
360 });
361 services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
362 }
363
364 // add the correct database context type
365 var dbType = postSetupServices.DatabaseConfiguration.DatabaseType;
366 switch (dbType)
367 {
368 case DatabaseType.MySql:
369 case DatabaseType.MariaDB:
370 AddTypedContext<MySqlDatabaseContext>();
371 break;
372 case DatabaseType.SqlServer:
373 AddTypedContext<SqlServerDatabaseContext>();
374 break;
375 case DatabaseType.Sqlite:
376 AddTypedContext<SqliteDatabaseContext>();
377 break;
378 case DatabaseType.PostgresSql:
379 AddTypedContext<PostgresSqlDatabaseContext>();
380 break;
381 default:
382 throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
383 }
384
385 // configure other database services
386 services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
387 services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
388
389 // configure other security services
390 services.AddSingleton<IOAuthProviders, OAuthProviders>();
391 services.AddSingleton<IIdentityCache, IdentityCache>();
392 services.AddSingleton<ICryptographySuite, CryptographySuite>();
393 services.AddSingleton<ITokenFactory, TokenFactory>();
395 services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
396
397 // configure platform specific services
398 if (postSetupServices.PlatformIdentifier.IsWindows)
399 {
400 AddWatchdog<WindowsWatchdogFactory>(services, postSetupServices);
403 services.AddSingleton<ByondInstallerBase, WindowsByondInstaller>();
404 services.AddSingleton<OpenDreamInstaller, WindowsOpenDreamInstaller>();
405 services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
406 services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
407
408 services.AddSingleton<WindowsNetworkPromptReaper>();
409 services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
410 services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
411 }
412 else
413 {
414 AddWatchdog<PosixWatchdogFactory>(services, postSetupServices);
415 services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
416 services.AddSingleton<IFilesystemLinkFactory, PosixFilesystemLinkFactory>();
417 services.AddSingleton<ByondInstallerBase, PosixByondInstaller>();
418 services.AddSingleton<OpenDreamInstaller>();
419 services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
420
421 services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
422 services.AddHostedService<PosixProcessFeatures>();
423
424 // PosixProcessFeatures also needs a IProcessExecutor for gcore
425 services.AddSingleton(x => new Lazy<IProcessExecutor>(() => x.GetRequiredService<IProcessExecutor>(), true));
426 services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
427
428 services.AddHostedService<PosixSignalHandler>();
429 }
430
431 // only global repo manager should be for the OD repo
432 // god help me if we need more
433 var openDreamRepositoryDirectory = ioManager.ConcatPath(
434 ioManager.GetPathInLocalDirectory(assemblyInformationProvider),
435 "OpenDreamRepository");
436 services.AddSingleton(
437 services => services
438 .GetRequiredService<IRepositoryManagerFactory>()
439 .CreateRepositoryManager(
441 services.GetRequiredService<IIOManager>(),
442 openDreamRepositoryDirectory),
443 new NoopEventConsumer()));
444
445 services.AddSingleton(
446 serviceProvider => new Dictionary<EngineType, IEngineInstaller>
447 {
448 { EngineType.Byond, serviceProvider.GetRequiredService<ByondInstallerBase>() },
449 { EngineType.OpenDream, serviceProvider.GetRequiredService<OpenDreamInstaller>() },
450 }
451 .ToFrozenDictionary());
452 services.AddSingleton<IEngineInstaller, DelegatingEngineInstaller>();
453
454 if (postSetupServices.InternalConfiguration.UsingSystemD)
455 services.AddHostedService<SystemDManager>();
456
457 // configure file transfer services
458 services.AddSingleton<FileTransferService>();
459 services.AddSingleton<IFileTransferStreamHandler>(x => x.GetRequiredService<FileTransferService>());
460 services.AddSingleton<IFileTransferTicketProvider>(x => x.GetRequiredService<FileTransferService>());
462
463 // configure swarm service
464 services.AddSingleton<SwarmService>();
465 services.AddSingleton<ISwarmService>(x => x.GetRequiredService<SwarmService>());
466 services.AddSingleton<ISwarmOperations>(x => x.GetRequiredService<SwarmService>());
467 services.AddSingleton<ISwarmServiceController>(x => x.GetRequiredService<SwarmService>());
468
469 // configure component services
470 services.AddSingleton<IPortAllocator, PortAllocator>();
471 services.AddSingleton<IInstanceFactory, InstanceFactory>();
472 services.AddSingleton<IGitRemoteFeaturesFactory, GitRemoteFeaturesFactory>();
473 services.AddSingleton<ILibGit2RepositoryFactory, LibGit2RepositoryFactory>();
474 services.AddSingleton<ILibGit2Commands, LibGit2Commands>();
475 services.AddSingleton<IRepositoryManagerFactory, RepostoryManagerFactory>();
477 services.AddChatProviderFactory();
478 services.AddSingleton<IChatManagerFactory, ChatManagerFactory>();
479 services.AddSingleton<IServerUpdater, ServerUpdater>();
480 services.AddSingleton<IServerUpdateInitiator, ServerUpdateInitiator>();
481 services.AddSingleton<IDotnetDumpService, DotnetDumpService>();
482
483 // configure authorities
484 services.AddScoped(typeof(IRestAuthorityInvoker<>), typeof(RestAuthorityInvoker<>));
485 services.AddScoped(typeof(IGraphQLAuthorityInvoker<>), typeof(GraphQLAuthorityInvoker<>));
486 services.AddScoped<ILoginAuthority, LoginAuthority>();
487 services.AddScoped<IUserAuthority, UserAuthority>();
488 services.AddScoped<IUserGroupAuthority, UserGroupAuthority>();
489 services.AddScoped<IPermissionSetAuthority, PermissionSetAuthority>();
490
491 // configure misc services
492 services.AddSingleton<IProcessExecutor, ProcessExecutor>();
493 services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
494 services.AddSingleton<IServerPortProvider, ServerPortProivder>();
495 services.AddSingleton<ITopicClientFactory, TopicClientFactory>();
496 services.AddHostedService<CommandPipeManager>();
497 services.AddHostedService<VersionReportingService>();
498
499 services.AddFileDownloader();
500 services.AddGitHub();
501
502 // configure root services
503 services.AddSingleton<JobService>();
504 services.AddSingleton<IJobService>(provider => provider.GetRequiredService<JobService>());
505 services.AddSingleton<IJobsHubUpdater>(provider => provider.GetRequiredService<JobService>());
506 services.AddSingleton<IJobManager>(x => x.GetRequiredService<IJobService>());
507 services.AddSingleton<JobsHubGroupMapper>();
508 services.AddSingleton<IPermissionsUpdateNotifyee>(provider => provider.GetRequiredService<JobsHubGroupMapper>());
509 services.AddSingleton<IHostedService>(x => x.GetRequiredService<JobsHubGroupMapper>()); // bit of a hack, but we need this to load immediated
510
511 services.AddSingleton<InstanceManager>();
512 services.AddSingleton<IBridgeDispatcher>(x => x.GetRequiredService<InstanceManager>());
513 services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
514 }
515
529 public void Configure(
530 IApplicationBuilder applicationBuilder,
531 IServerControl serverControl,
533 IServerPortProvider serverPortProvider,
534 IAssemblyInformationProvider assemblyInformationProvider,
535 IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
536 IOptions<GeneralConfiguration> generalConfigurationOptions,
537 IOptions<SwarmConfiguration> swarmConfigurationOptions,
538 IOptions<InternalConfiguration> internalConfigurationOptions,
539 ILogger<Application> logger)
540 {
541 ArgumentNullException.ThrowIfNull(applicationBuilder);
542 ArgumentNullException.ThrowIfNull(serverControl);
543
544 this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
545
546 ArgumentNullException.ThrowIfNull(serverPortProvider);
547 ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
548
549 var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
550 var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
551 var swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
552 var internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions));
553
554 ArgumentNullException.ThrowIfNull(logger);
555
556 logger.LogDebug("Content Root: {contentRoot}", hostingEnvironment.ContentRootPath);
557 logger.LogTrace("Web Root: {webRoot}", hostingEnvironment.WebRootPath);
558
559 // setup the HTTP request pipeline
560 // Add additional logging context to the request
561 applicationBuilder.UseAdditionalRequestLoggingContext(swarmConfiguration);
562
563 // Wrap exceptions in a 500 (ErrorMessage) response
564 applicationBuilder.UseServerErrorHandling();
565
566 // Add the X-Powered-By response header
567 applicationBuilder.UseServerBranding(assemblyInformationProvider);
568
569 // Add the X-Accel-Buffering response header
570 applicationBuilder.UseDisabledNginxProxyBuffering();
571
572 // suppress OperationCancelledExceptions, they are just aborted HTTP requests
573 applicationBuilder.UseCancelledRequestSuppression();
574
575 // 503 requests made while the application is starting
576 applicationBuilder.UseAsyncInitialization<IInstanceManager>(
577 (instanceManager, cancellationToken) => instanceManager.Ready.WaitAsync(cancellationToken));
578
579 if (generalConfiguration.HostApiDocumentation)
580 {
581 var siteDocPath = Routes.ApiRoot + $"doc/{SwaggerConfiguration.DocumentName}.json";
582 if (!String.IsNullOrWhiteSpace(controlPanelConfiguration.PublicPath))
583 siteDocPath = controlPanelConfiguration.PublicPath.TrimEnd('/') + siteDocPath;
584
585 applicationBuilder.UseSwagger(options =>
586 {
587 options.RouteTemplate = Routes.ApiRoot + "doc/{documentName}.{json|yaml}";
588 });
589 applicationBuilder.UseSwaggerUI(options =>
590 {
592 options.SwaggerEndpoint(siteDocPath, "TGS API");
593 });
594 logger.LogTrace("Swagger API generation enabled");
595 }
596
597 // spa loading if necessary
598 if (controlPanelConfiguration.Enable)
599 {
600 logger.LogInformation("Web control panel enabled.");
601 applicationBuilder.UseFileServer(new FileServerOptions
602 {
604 EnableDefaultFiles = true,
605 EnableDirectoryBrowsing = false,
606 RedirectToAppendTrailingSlash = false,
607 });
608 }
609 else
610#if NO_WEBPANEL
611 logger.LogDebug("Web control panel was not included in TGS build!");
612#else
613 logger.LogTrace("Web control panel disabled!");
614#endif
615
616 // Enable endpoint routing
617 applicationBuilder.UseRouting();
618
619 // Set up CORS based on configuration if necessary
620 Action<CorsPolicyBuilder>? corsBuilder = null;
621 if (controlPanelConfiguration.AllowAnyOrigin)
622 {
623 logger.LogTrace("Access-Control-Allow-Origin: *");
624 corsBuilder = builder => builder.SetIsOriginAllowed(_ => true);
625 }
626 else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
627 {
628 logger.LogTrace("Access-Control-Allow-Origin: {allowedOrigins}", String.Join(',', controlPanelConfiguration.AllowedOrigins));
629 corsBuilder = builder => builder.WithOrigins([.. controlPanelConfiguration.AllowedOrigins]);
630 }
631
632 var originalBuilder = corsBuilder;
633 corsBuilder = builder =>
634 {
635 builder
636 .AllowAnyHeader()
637 .AllowAnyMethod()
638 .AllowCredentials()
639 .SetPreflightMaxAge(TimeSpan.FromDays(1));
640 originalBuilder?.Invoke(builder);
641 };
642 applicationBuilder.UseCors(corsBuilder);
643
644 // validate the API version
645 applicationBuilder.UseApiCompatibility();
646
647 // authenticate JWT tokens using our security pipeline if present, returns 401 if bad
648 applicationBuilder.UseAuthentication();
649
650 // enable authorization on endpoints
651 applicationBuilder.UseAuthorization();
652
653 // suppress and log database exceptions
654 applicationBuilder.UseDbConflictHandling();
655
656 // setup endpoints
657 applicationBuilder.UseEndpoints(endpoints =>
658 {
659 // access to the signalR jobs hub
660 endpoints.MapHub<JobsHub>(
662 options =>
663 {
664 options.Transports = HttpTransportType.ServerSentEvents;
665 options.CloseOnAuthenticationExpiration = true;
666 })
667 .RequireAuthorization()
668 .RequireCors(corsBuilder);
669
670 // majority of handling is done in the controllers
671 endpoints.MapControllers();
672
673 if (internalConfiguration.EnableGraphQL)
674 {
675 logger.LogWarning("Enabling GraphQL. This API is experimental and breaking changes may occur at any time!");
676 var gqlOptions = new GraphQLServerOptions
677 {
678 EnableBatching = true,
679 };
680
681 gqlOptions.Tool.Enable = generalConfiguration.HostApiDocumentation;
682
683 endpoints
684 .MapGraphQL(Routes.GraphQL)
685 .WithOptions(gqlOptions);
686 }
687 });
688
689 // 404 anything that gets this far
690 // End of request pipeline setup
691 logger.LogTrace("Configuration version: {configVersion}", GeneralConfiguration.CurrentConfigVersion);
692 logger.LogTrace("DMAPI Interop version: {interopVersion}", DMApiConstants.InteropVersion);
693 if (controlPanelConfiguration.Enable)
694 logger.LogTrace("Webpanel version: {webCPVersion}", MasterVersionsAttribute.Instance.RawWebpanelVersion);
695
696 logger.LogDebug("Starting hosting on port {httpApiPort}...", serverPortProvider.HttpApiPort);
697 }
698
700 protected override void ConfigureHostedService(IServiceCollection services)
701 => services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
702
707 void ConfigureAuthenticationPipeline(IServiceCollection services)
708 {
709 services.AddHttpContextAccessor();
710 services.AddScoped<IApiHeadersProvider, ApiHeadersProvider>();
711 services.AddScoped<AuthenticationContextFactory>();
712 services.AddScoped<ITokenValidator>(provider => provider.GetRequiredService<AuthenticationContextFactory>());
713
714 // what if you
715 // wanted to just do this:
716 // return provider.GetRequiredService<AuthenticationContextFactory>().CurrentAuthenticationContext
717 // But M$ said
718 // https://stackoverflow.com/questions/56792917/scoped-services-in-asp-net-core-with-signalr-hubs
719 services.AddScoped(provider => (provider
720 .GetRequiredService<IHttpContextAccessor>()
721 .HttpContext ?? throw new InvalidOperationException($"Unable to resolve {nameof(IAuthenticationContext)} due to no HttpContext being available!"))
722 .RequestServices
723 .GetRequiredService<AuthenticationContextFactory>()
724 .CurrentAuthenticationContext);
726
727 services
728 .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
729 .AddJwtBearer(jwtBearerOptions =>
730 {
731 // this line isn't actually run until the first request is made
732 // at that point tokenFactory will be populated
733 jwtBearerOptions.TokenValidationParameters = tokenFactory?.ValidationParameters ?? throw new InvalidOperationException("tokenFactory not initialized!");
734 jwtBearerOptions.MapInboundClaims = false;
735 jwtBearerOptions.Events = new JwtBearerEvents
736 {
737 OnMessageReceived = context =>
738 {
739 if (String.IsNullOrWhiteSpace(context.Token))
740 {
741 var accessToken = context.Request.Query["access_token"];
742 var path = context.HttpContext.Request.Path;
743
744 if (!String.IsNullOrWhiteSpace(accessToken) &&
745 path.StartsWithSegments(Routes.HubsRoot, StringComparison.OrdinalIgnoreCase))
746 {
747 context.Token = accessToken;
748 }
749 }
750
751 return Task.CompletedTask;
752 },
753 OnTokenValidated = context => context
754 .HttpContext
755 .RequestServices
756 .GetRequiredService<ITokenValidator>()
757 .ValidateToken(
758 context,
759 context
760 .HttpContext
761 .RequestAborted),
762 };
763 });
764 }
765 }
766}
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...
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 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.