/// <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();
        }
Пример #2
0
        /// <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;
            }
        }
Пример #4
0
        /// <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));
            }
        }
Пример #5
0
        /// <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));
            }
        }