/// <inheritdoc /> public Task ProcessPayload(PullRequestEventPayload payload, CancellationToken cancellationToken) { if (payload == null) { throw new ArgumentNullException(nameof(payload)); } switch (payload.Action) { case "opened": case "closed": case "reopened": //reserialize it //.htmlSpecialChars($payload['sender']['login']).': <a href="'.$payload['pull_request']['html_url'].'">'.htmlSpecialChars('#'.$payload['pull_request']['number'].' '.$payload['pull_request']['user']['login'].' - '.$payload['pull_request']['title']).'</a>'; var json = new SimpleJsonSerializer().Serialize(payload); json = byondTopicSender.SanitizeString(json); const string innerAnnouncementFormatter = "#{0} {1} - {2}"; var announcement = String.Format(CultureInfo.CurrentCulture, innerAnnouncementFormatter, payload.PullRequest.Number, payload.PullRequest.User.Login, payload.PullRequest.Title); announcement = HttpUtility.HtmlEncode(announcement); var announcmentFormatter = stringLocalizer["AnnouncementFormatter"]; // "[{0}] Pull Request {1} by {2}: <a href='{3}'>{4}</a>"; announcement = String.Format(CultureInfo.CurrentCulture, announcmentFormatter, payload.Repository.FullName, payload.PullRequest.Merged ? stringLocalizer["Merged"] : payload.Action, payload.Sender.Login, payload.PullRequest.HtmlUrl, announcement); announcement = byondTopicSender.SanitizeString(announcement); var startingQuery = String.Format(CultureInfo.InvariantCulture, "?announce={0}&payload={1}&key=", json, announcement); var tasks = new List <Task>(); foreach (var I in serverConfiguration.Entries) { var final = startingQuery + byondTopicSender.SanitizeString(I.CommsKey); tasks.Add(byondTopicSender.SendTopic(I.Address, I.Port, final, cancellationToken)); } return(Task.WhenAll(tasks)); } throw new NotSupportedException(); }
/// <inheritdoc /> public async Task <bool> HandleEvent(EventType eventType, IEnumerable <string> parameters, CancellationToken cancellationToken) { if (!Running) { return(true); } string results; using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) { if (!Running) { return(true); } var builder = new StringBuilder(Constants.DMTopicEvent); builder.Append('&'); var notification = new EventNotification { Type = eventType, Parameters = parameters }; var json = JsonConvert.SerializeObject(notification); builder.Append(byondTopicSender.SanitizeString(Constants.DMParameterData)); builder.Append('='); builder.Append(byondTopicSender.SanitizeString(json)); var activeServer = GetActiveController(); results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false); } if (results == Constants.DMResponseSuccess) { return(true); } List <Response> responses; try { responses = JsonConvert.DeserializeObject <List <Response> >(results); } catch { Logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results); return(true); } await Task.WhenAll(responses.Select(x => Chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false); return(true); }
/// <inheritdoc /> #pragma warning disable CA1506 // TODO: Decomplexify public async Task <ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken) { var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort; if (!portToUse.HasValue) { throw new InvalidOperationException("Given port is null!"); } var accessIdentifier = cryptographySuite.GetSecureString(); const string JsonPostfix = "tgs.json"; var basePath = primaryDirectory ? dmbProvider.PrimaryDirectory : dmbProvider.SecondaryDirectory; // delete all previous tgs json files var files = await ioManager.GetFilesWithExtension(basePath, JsonPostfix, cancellationToken).ConfigureAwait(false); await Task.WhenAll(files.Select(x => ioManager.DeleteFile(x, cancellationToken))).ConfigureAwait(false); // i changed this back from guids, hopefully i don't regret that string JsonFile(string name) => String.Format(CultureInfo.InvariantCulture, "{0}.{1}", name, JsonPostfix); var securityLevelToUse = launchParameters.SecurityLevel.Value; switch (dmbProvider.CompileJob.MinimumSecurityLevel) { case DreamDaemonSecurity.Ultrasafe: break; case DreamDaemonSecurity.Safe: if (securityLevelToUse == DreamDaemonSecurity.Ultrasafe) { securityLevelToUse = DreamDaemonSecurity.Safe; } break; case DreamDaemonSecurity.Trusted: securityLevelToUse = DreamDaemonSecurity.Trusted; break; default: throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel)); } // setup interop files var interopInfo = new JsonFile { AccessIdentifier = accessIdentifier, ApiValidateOnly = apiValidate, ChatChannelsJson = JsonFile("chat_channels"), ChatCommandsJson = JsonFile("chat_commands"), ServerCommandsJson = JsonFile("server_commands"), InstanceName = instance.Name, SecurityLevel = securityLevelToUse, Revision = new Api.Models.Internal.RevisionInformation { CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha, OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha } }; interopInfo.TestMerges.AddRange(dmbProvider.CompileJob.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => new Interop.TestMerge(x, interopInfo.Revision))); var interopJsonFile = JsonFile("interop"); var interopJson = JsonConvert.SerializeObject(interopInfo, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver(), ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); var chatJsonTrackingTask = chat.TrackJsons(basePath, interopInfo.ChatChannelsJson, interopInfo.ChatCommandsJson, cancellationToken); await ioManager.WriteAllBytes(ioManager.ConcatPath(basePath, interopJsonFile), Encoding.UTF8.GetBytes(interopJson), cancellationToken).ConfigureAwait(false); var chatJsonTrackingContext = await chatJsonTrackingTask.ConfigureAwait(false); try { // get the byond lock var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false); try { // create interop context var context = new CommContext(ioManager, loggerFactory.CreateLogger <CommContext>(), basePath, interopInfo.ServerCommandsJson); try { // set command line options // more sanitization here cause it uses the same scheme var parameters = String.Format(CultureInfo.InvariantCulture, "{2}={0}&{3}={1}", byondTopicSender.SanitizeString(application.Version.ToString()), byondTopicSender.SanitizeString(interopJsonFile), byondTopicSender.SanitizeString(Constants.DMParamHostVersion), byondTopicSender.SanitizeString(Constants.DMParamInfoJson)); var visibility = apiValidate ? "invisible" : "public"; // important to run on all ports to allow port changing var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -{3} -{5} -public -params \"{4}\"", dmbProvider.DmbName, primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort, launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty, SecurityWord(securityLevelToUse), parameters, visibility); // See https://github.com/tgstation/tgstation-server/issues/719 var noShellExecute = !platformIdentifier.IsWindows; // launch dd var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments, noShellExecute: noShellExecute); try { networkPromptReaper.RegisterProcess(process); // return the session controller for it var result = new SessionController(new ReattachInformation { AccessIdentifier = accessIdentifier, Dmb = dmbProvider, IsPrimary = primaryDirectory, Port = portToUse.Value, ProcessId = process.Id, ChatChannelsJson = interopInfo.ChatChannelsJson, ChatCommandsJson = interopInfo.ChatCommandsJson, ServerCommandsJson = interopInfo.ServerCommandsJson, }, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger <SessionController>(), launchParameters.SecurityLevel, launchParameters.StartupTimeout); // writeback launch parameter's fixed security level launchParameters.SecurityLevel = securityLevelToUse; return(result); } catch { process.Dispose(); throw; } } catch { context.Dispose(); throw; } } catch { if (currentByondLock == null) { byondLock.Dispose(); } throw; } } catch { chatJsonTrackingContext.Dispose(); throw; } }
/// <inheritdoc /> #pragma warning disable CA1502 // TODO: Decomplexify public async Task HandleInterop(CommCommand command, CancellationToken cancellationToken) { if (command == null) { throw new ArgumentNullException(nameof(command)); } var query = command.Parameters; object content; Action postRespond = null; ushort?overrideResponsePort = null; if (query.TryGetValue(Constants.DMParameterCommand, out var method)) { content = new object(); switch (method) { case Constants.DMCommandChat: try { var message = JsonConvert.DeserializeObject <Response>(command.RawJson, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); if (message.ChannelIds == null) { throw new InvalidOperationException("Missing ChannelIds field!"); } if (message.Message == null) { throw new InvalidOperationException("Missing Message field!"); } await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(false); } catch (Exception e) { logger.LogDebug("Exception while decoding chat message! Exception: {0}", e); goto default; } break; case Constants.DMCommandServerPrimed: // currently unused, maybe in the future break; case Constants.DMCommandEndProcess: TerminationWasRequested = true; process.Terminate(); return; case Constants.DMCommandNewPort: lock (this) { if (!query.TryGetValue(Constants.DMParameterData, out var stringPortObject) || !UInt16.TryParse(stringPortObject as string, out var currentPort)) { /////UHHHH logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); content = new ErrorMessage { Message = "Missing stringified port as data parameter!" }; break; } if (!nextPort.HasValue) { reattachInformation.Port = currentPort; // not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to } else { // nextPort is ready, tell DD to switch to that // if it fails it'll kill itself content = new Dictionary <string, ushort> { { Constants.DMParameterData, nextPort.Value } }; reattachInformation.Port = nextPort.Value; overrideResponsePort = currentPort; nextPort = null; // we'll also get here from SetPort so complete that task var tmpTcs = portAssignmentTcs; portAssignmentTcs = null; if (tmpTcs != null) { postRespond = () => tmpTcs.SetResult(true); } } portClosedForReboot = false; } break; case Constants.DMCommandApiValidate: 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 stringMinimumSecurityLevelObject) || !Enum.TryParse <DreamDaemonSecurity>(stringMinimumSecurityLevelObject as string, out var minimumSecurityLevel)) { apiValidationStatus = ApiValidationStatus.BadValidationRequest; } else { switch (minimumSecurityLevel) { case DreamDaemonSecurity.Safe: apiValidationStatus = ApiValidationStatus.RequiresSafe; break; case DreamDaemonSecurity.Ultrasafe: apiValidationStatus = ApiValidationStatus.RequiresUltrasafe; break; case DreamDaemonSecurity.Trusted: apiValidationStatus = ApiValidationStatus.RequiresTrusted; break; default: throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!"); } } break; case Constants.DMCommandWorldReboot: if (ClosePortOnReboot) { chatJsonTrackingContext.Active = false; content = new Dictionary <string, int> { { Constants.DMParameterData, 0 } }; portClosedForReboot = true; } var oldTcs = rebootTcs; rebootTcs = new TaskCompletionSource <object>(); postRespond = () => oldTcs.SetResult(null); break; default: content = new ErrorMessage { Message = "Requested command not supported!" }; break; } } else { content = new ErrorMessage { Message = "Missing command parameter!" } }; var json = JsonConvert.SerializeObject(content); var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), overrideResponsePort, cancellationToken).ConfigureAwait(false); if (response != Constants.DMResponseSuccess) { logger.LogWarning("Received error response while responding to interop: {0}", response); } postRespond?.Invoke(); } #pragma warning restore CA1502 /// <summary> /// Throws an <see cref="ObjectDisposedException"/> if <see cref="Dispose(bool)"/> has been called /// </summary> void CheckDisposed() { if (disposed) { throw new ObjectDisposedException(nameof(SessionController)); } }
/// <inheritdoc /> public async Task HandleInterop(CommCommand command, CancellationToken cancellationToken) { if (command == null) { throw new ArgumentNullException(nameof(command)); } var query = command.Parameters; object content; Action postRespond = null; if (query.TryGetValue(Constants.DMParameterCommand, out var method)) { content = new object(); switch (method) { case Constants.DMCommandServerPrimed: //currently unused, maybe in the future break; case Constants.DMCommandEndProcess: TerminationWasRequested = true; process.Terminate(); return; case Constants.DMCommandNewPort: lock (this) { if (!query.TryGetValue(Constants.DMParameterData, out var stringPort) || !UInt16.TryParse(stringPort, out var currentPort)) { /////UHHHH logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); break; } if (!nextPort.HasValue) { //not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to reattachInformation.Port = currentPort; } else { //nextPort is ready, tell DD to switch to that //if it fails it'll kill itself content = new Dictionary <string, ushort> { { Constants.DMParameterData, nextPort.Value } }; reattachInformation.Port = nextPort.Value; nextPort = null; //we'll also get here from SetPort so complete that task var tmpTcs = portAssignmentTcs; portAssignmentTcs = null; if (tmpTcs != null) { postRespond = () => tmpTcs.SetResult(true); } } portClosedForReboot = false; } break; case Constants.DMCommandApiValidate: apiValidated = true; break; case Constants.DMCommandWorldReboot: if (ClosePortOnReboot) { content = new Dictionary <string, int> { { Constants.DMParameterData, 0 } }; portClosedForReboot = true; } var oldTcs = rebootTcs; rebootTcs = new TaskCompletionSource <object>(); postRespond = () => oldTcs.SetResult(null); break; default: content = new ErrorMessage { Message = "Requested command not supported!" }; break; } } else { content = new ErrorMessage { Message = "Missing command parameter!" } }; var json = JsonConvert.SerializeObject(content); var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), cancellationToken).ConfigureAwait(false); if (response != Constants.DMResponseSuccess) { logger.LogWarning("Recieved error response while responding to interop: {0}", response); } postRespond?.Invoke(); } /// <summary> /// Throws an <see cref="ObjectDisposedException"/> if <see cref="Dispose(bool)"/> has been called /// </summary> void CheckDisposed() { if (disposed) { throw new ObjectDisposedException(nameof(SessionController)); } }