From 5a0acf7266cff7a40c731bdbd1189feb0126db4c Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 17 Sep 2018 16:21:43 -0400 Subject: [PATCH] Add minimum security level validation to the compile step. Only serves as a one off warning at the moment. To be integrated with compile jobs and expanded upon at a later date. At the moment, this also allows for more verbosity during DMAPI validation. --- src/DMAPI/tgs/v4/api.dm | 2 +- .../Components/Compiler/DreamMaker.cs | 39 ++++++++++++---- .../Watchdog/ApiValidationStatus.cs | 33 +++++++++++++ .../Components/Watchdog/ISessionController.cs | 4 +- .../Components/Watchdog/SessionController.cs | 46 ++++++++++++++++--- .../Watchdog/SessionControllerFactory.cs | 4 +- 6 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Watchdog/ApiValidationStatus.cs diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm index 4fb8afc385..bf1f506f0a 100644 --- a/src/DMAPI/tgs/v4/api.dm +++ b/src/DMAPI/tgs/v4/api.dm @@ -68,7 +68,7 @@ if(cached_json["apiValidateOnly"]) TGS_INFO_LOG("Validating API and exiting...") - Export(TGS4_COMM_VALIDATE, list(TGS4_PARAMETER_DATA = minimum_required_security_level)) + Export(TGS4_COMM_VALIDATE, list(TGS4_PARAMETER_DATA = "[minimum_required_security_level]")) del(world) chat_channels_json_path = cached_json["chatChannelsJson"] diff --git a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs index 15c46067f8..e8dd38d401 100644 --- a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs @@ -125,8 +125,8 @@ namespace Tgstation.Server.Host.Components.Compiler /// The current /// The port to use for API validation /// The for the operation - /// A resulting in if the DMAPI was successfully validated, otherwise - async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken) + /// A representing the running operation + async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken) { logger.LogTrace("Verifying DMAPI..."); var launchParameters = new DreamDaemonLaunchParameters @@ -156,13 +156,27 @@ namespace Tgstation.Server.Host.Components.Compiler if (!controller.Lifetime.IsCompleted) { - logger.LogDebug("API validation timed out!"); - return false; + var validationStatus = controller.ApiValidationStatus; + logger.LogTrace("API validation status: {0}", validationStatus); + switch (validationStatus) + { + case ApiValidationStatus.Validated: + return; + case ApiValidationStatus.NeverValidated: + break; + case ApiValidationStatus.BadValidationRequest: + throw new JobException("Recieved an unrecognized API validation request from DreamDaemon!"); + case ApiValidationStatus.RequiresSafe: + throw new JobException("This game must be run with at least the 'Safe' DreamDaemon security level!"); + case ApiValidationStatus.RequiresTrusted: + throw new JobException("This game must be run with at least the 'Trusted' DreamDaemon security level!"); + case ApiValidationStatus.UnaskedValidationRequest: + default: + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Session controller returned unexpected ApiValidationStatus: {0}", validationStatus)); + } } - var validated = controller.ApiValidated; - logger.LogTrace("API valid: {0}", validated); - return validated; + throw new JobException("DMAPI validation timed out!"); } } @@ -356,13 +370,18 @@ namespace Tgstation.Server.Host.Components.Compiler var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); - var apiValidated = exitCode == 0 && await VerifyApi(apiValidateTimeout, securityLevel, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false); + try + { + if (exitCode != 0) + throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output)); - if (!apiValidated) + await VerifyApi(apiValidateTimeout, securityLevel, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false); + } + catch (JobException) { //server never validated or compile failed await eventConsumer.HandleEvent(EventType.CompileFailure, new List { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false); - throw new JobException(exitCode == 0 ? "Validation of the TGS api failed!" : String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output)); + throw; } logger.LogTrace("Running post compile event..."); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ApiValidationStatus.cs b/src/Tgstation.Server.Host/Components/Watchdog/ApiValidationStatus.cs new file mode 100644 index 0000000000..cc179c5271 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Watchdog/ApiValidationStatus.cs @@ -0,0 +1,33 @@ +namespace Tgstation.Server.Host.Components.Watchdog +{ + /// + /// Status of DMAPI validation + /// + enum ApiValidationStatus + { + /// + /// The DMAPI never contacted the server for validation + /// + NeverValidated, + /// + /// The server was contacted for validation but it was never requested + /// + UnaskedValidationRequest, + /// + /// The game must be run with a minimum security level of + /// + RequiresSafe, + /// + /// The game must be run with a security level of + /// + RequiresTrusted, + /// + /// The validation request was malformed + /// + BadValidationRequest, + /// + /// The DMAPI validated successfully + /// + Validated + } +} diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs index 3f2e245eba..97e19b271d 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs @@ -25,9 +25,9 @@ namespace Tgstation.Server.Host.Components.Watchdog bool TerminationWasRequested { get; } /// - /// If the DMAPI was validated. This field may only be access once completes + /// The DMAPI /// - bool ApiValidated { get; } + ApiValidationStatus ApiValidationStatus { get; } /// /// The being used diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs index 20b1115531..969aed7866 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs @@ -29,13 +29,13 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public bool ApiValidated + public ApiValidationStatus ApiValidationStatus { get { if (!Lifetime.IsCompleted) throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!"); - return apiValidated; + return apiValidationStatus; } } @@ -126,6 +126,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly ILogger logger; + /// + /// The level the was launched with + /// + readonly DreamDaemonSecurity? launchSecurityLevel; + /// /// The waits on when DreamDaemon currently has it's ports closed /// @@ -151,9 +156,9 @@ namespace Tgstation.Server.Host.Components.Watchdog bool disposed; /// - /// If the DMAPI was validated + /// The for the /// - bool apiValidated; + ApiValidationStatus apiValidationStatus; /// /// If should be kept alive instead @@ -171,8 +176,9 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of + /// The value of /// The optional time to wait before failing the - public SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger logger, uint? startupTimeout) + public SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger logger, DreamDaemonSecurity? launchSecurityLevel, uint? startupTimeout) { this.chatJsonTrackingContext = chatJsonTrackingContext; //null valid this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); @@ -183,11 +189,13 @@ namespace Tgstation.Server.Host.Components.Watchdog this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.launchSecurityLevel = launchSecurityLevel; + interopContext.RegisterHandler(this); portClosedForReboot = false; disposed = false; - apiValidated = false; + apiValidationStatus = ApiValidationStatus.NeverValidated; released = false; rebootTcs = new TaskCompletionSource(); @@ -289,6 +297,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { /////UHHHH logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); + content = new ErrorMessage { Message = "Missing stringified port as data parameter!" }; break; } @@ -314,7 +323,30 @@ namespace Tgstation.Server.Host.Components.Watchdog } break; case Constants.DMCommandApiValidate: - apiValidated = true; + if (!launchSecurityLevel.HasValue) + { + logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!"); + apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest; + content = new ErrorMessage { Message = "Invalid API validation request!" }; + break; + } + if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevel) || !Enum.TryParse(stringMinimumSecurityLevel, out var minimumSecurityLevel)) + apiValidationStatus = ApiValidationStatus.BadValidationRequest; + else + switch (minimumSecurityLevel) + { + case DreamDaemonSecurity.Safe: + apiValidationStatus = launchSecurityLevel == DreamDaemonSecurity.Ultrasafe ? ApiValidationStatus.RequiresSafe : ApiValidationStatus.Validated; + break; + case DreamDaemonSecurity.Ultrasafe: + apiValidationStatus = ApiValidationStatus.Validated; + break; + case DreamDaemonSecurity.Trusted: + apiValidationStatus = launchSecurityLevel == DreamDaemonSecurity.Trusted ? ApiValidationStatus.Validated : ApiValidationStatus.RequiresTrusted; + break; + default: + throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!"); + } break; case Constants.DMCommandWorldReboot: if (ClosePortOnReboot) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs index fad899d6e7..b2f4399c13 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs @@ -200,7 +200,7 @@ namespace Tgstation.Server.Host.Components.Watchdog ChatChannelsJson = interopInfo.ChatChannelsJson, ChatCommandsJson = interopInfo.ChatCommandsJson, ServerCommandsJson = interopInfo.ServerCommandsJson, - }, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger(), launchParameters.StartupTimeout); + }, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger(), launchParameters.SecurityLevel, launchParameters.StartupTimeout); } catch { @@ -247,7 +247,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var process = processExecutor.GetProcess(reattachInformation.ProcessId); try { - return new SessionController(reattachInformation, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger(), null); + return new SessionController(reattachInformation, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger(), null, null); } catch {