diff --git a/Application/Main.cs b/Application/Main.cs index ff1741487..4971fa1fd 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -452,6 +452,7 @@ namespace IW4MAdmin.Application .AddSingleton() .AddTransient() .AddSingleton() + .AddSingleton() .AddSingleton(translationLookup) .AddDatabaseContextOptions(appConfig); diff --git a/Application/Misc/InteractionRegistration.cs b/Application/Misc/InteractionRegistration.cs index 66d5e14c1..efe02edbc 100644 --- a/Application/Misc/InteractionRegistration.cs +++ b/Application/Misc/InteractionRegistration.cs @@ -89,8 +89,8 @@ public class InteractionRegistration : IInteractionRegistration }))).Where(interaction => interaction is not null); } - public async Task ProcessInteraction(string interactionId, int? clientId = null, - Reference.Game? game = null, CancellationToken token = default) + public async Task ProcessInteraction(string interactionId, int originId, int? targetId = null, + Reference.Game? game = null, IDictionary meta = null, CancellationToken token = default) { if (!_interactions.ContainsKey(interactionId)) { @@ -99,11 +99,11 @@ public class InteractionRegistration : IInteractionRegistration try { - var interaction = await _interactions[interactionId](clientId, game, token); + var interaction = await _interactions[interactionId](originId, game, token); if (interaction.Action is not null) { - return await interaction.Action(clientId, game, token); + return await interaction.Action(originId, targetId, game, meta, token); } if (interaction.ScriptAction is not null) @@ -115,16 +115,15 @@ public class InteractionRegistration : IInteractionRegistration continue; } - return scriptPlugin.ExecuteAction(interaction.ScriptAction, clientId, game, token); + return scriptPlugin.ExecuteAction(interaction.ScriptAction, originId, targetId, game, token); } } } catch (Exception ex) { _logger.LogWarning(ex, - "Could not process interaction for interaction {InteractionName} and ClientId {ClientId}", - interactionId, - clientId); + "Could not process interaction for interaction {InteractionName} and OriginId {ClientId}", + interactionId, originId); } return null; diff --git a/Application/Misc/RemoteCommandService.cs b/Application/Misc/RemoteCommandService.cs new file mode 100644 index 000000000..4e4bfc3ed --- /dev/null +++ b/Application/Misc/RemoteCommandService.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using SharedLibraryCore; +using SharedLibraryCore.Configuration; +using SharedLibraryCore.Dtos; +using SharedLibraryCore.Interfaces; +using SharedLibraryCore.Services; + +namespace IW4MAdmin.Application.Misc; + +public class RemoteCommandService : IRemoteCommandService +{ + private readonly ApplicationConfiguration _appConfig; + private readonly ClientService _clientService; + + public RemoteCommandService(ApplicationConfiguration appConfig, ClientService clientService) + { + _appConfig = appConfig; + _clientService = clientService; + } + + public async Task> Execute(int originId, int? targetId, string command, + IEnumerable arguments, Server server) + { + var client = await _clientService.Get(originId); + client.CurrentServer = server; + + command += $" {(targetId.HasValue ? $"@{targetId} " : "")}{string.Join(" ", arguments ?? Enumerable.Empty())}"; + + var remoteEvent = new GameEvent + { + Type = GameEvent.EventType.Command, + Data = command.StartsWith(_appConfig.CommandPrefix) || + command.StartsWith(_appConfig.BroadcastCommandPrefix) + ? command + : $"{_appConfig.CommandPrefix}{command}", + Origin = client, + Owner = server, + IsRemote = true + }; + + server.Manager.AddEvent(remoteEvent); + CommandResponseInfo[] response; + + try + { + // wait for the event to process + var completedEvent = + await remoteEvent.WaitAsync(Utilities.DefaultCommandTimeout, server.Manager.CancellationToken); + + if (completedEvent.FailReason == GameEvent.EventFailReason.Timeout) + { + response = new[] + { + new CommandResponseInfo() + { + ClientId = client.ClientId, + Response = Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"] + } + }; + } + + else + { + response = completedEvent.Output.Select(output => new CommandResponseInfo() + { + Response = output, + ClientId = client.ClientId + }).ToArray(); + } + } + + catch (System.OperationCanceledException) + { + response = new[] + { + new CommandResponseInfo + { + ClientId = client.ClientId, + Response = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RESTART_SUCCESS"] + } + }; + } + + return response; + } +} diff --git a/Plugins/Login/Plugin.cs b/Plugins/Login/Plugin.cs index 5b8116761..94713977b 100644 --- a/Plugins/Login/Plugin.cs +++ b/Plugins/Login/Plugin.cs @@ -51,7 +51,7 @@ namespace IW4MAdmin.Plugins.Login manager.CommandInterceptors.Add(gameEvent => { - if (gameEvent.Type != GameEvent.EventType.Command) + if (gameEvent.Type != GameEvent.EventType.Command || gameEvent.Extra is null) { return true; } diff --git a/Plugins/ScriptPlugins/VPNDetection.js b/Plugins/ScriptPlugins/VPNDetection.js index c1fae81bf..d8f46470c 100644 --- a/Plugins/ScriptPlugins/VPNDetection.js +++ b/Plugins/ScriptPlugins/VPNDetection.js @@ -1,12 +1,12 @@ let vpnExceptionIds = []; const commands = [{ - name: "whitelistvpn", - description: "whitelists a player's client id from VPN detection", - alias: "wv", - permission: "SeniorAdmin", + name: 'whitelistvpn', + description: 'whitelists a player\'s client id from VPN detection', + alias: 'wv', + permission: 'SeniorAdmin', targetRequired: true, arguments: [{ - name: "player", + name: 'player', required: true }], execute: (gameEvent) => { @@ -19,12 +19,11 @@ const commands = [{ const plugin = { author: 'RaidMax', - version: 1.4, + version: 1.5, name: 'VPN Detection Plugin', manager: null, logger: null, - checkForVpn: function (origin) { let exempt = false; // prevent players that are exempt from being kicked @@ -80,24 +79,24 @@ const plugin = { this.logger = manager.GetLogger(0); this.configHandler = _configHandler; - this.configHandler.GetValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(element)); + this.configHandler.GetValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(parseInt(element))); this.logger.WriteInfo(`Loaded ${vpnExceptionIds.length} ids into whitelist`); this.interactionRegistration = _serviceResolver.ResolveService('IInteractionRegistration'); - this.interactionRegistration.RegisterScriptInteraction('WhitelistVPN', this.name, (clientId, game, token) => { - if (vpnExceptionIds.includes(clientId)) { + this.interactionRegistration.RegisterScriptInteraction('WhitelistVPN', this.name, (originId, targetId, game, token) => { + if (vpnExceptionIds.includes(targetId)) { return; } const helpers = importNamespace('SharedLibraryCore.Helpers'); const interactionData = new helpers.InteractionData(); - interactionData.EntityId = clientId; + interactionData.EntityId = targetId; interactionData.Name = 'Whitelist VPN'; interactionData.DisplayMeta = 'oi-circle-check'; interactionData.ActionMeta.Add('InteractionId', 'command'); - interactionData.ActionMeta.Add('Data', `whitelistvpn @${clientId}`); + interactionData.ActionMeta.Add('Data', `whitelistvpn`); interactionData.ActionMeta.Add('ActionButtonLabel', 'Allow'); interactionData.ActionMeta.Add('Name', 'Allow VPN Connection'); interactionData.ActionMeta.Add('ShouldRefresh', true.toString()); diff --git a/SharedLibraryCore/Helpers/InteractionData.cs b/SharedLibraryCore/Helpers/InteractionData.cs index b29ffb21a..68a5ab16d 100644 --- a/SharedLibraryCore/Helpers/InteractionData.cs +++ b/SharedLibraryCore/Helpers/InteractionData.cs @@ -3,8 +3,7 @@ using System.Collections.Generic; using System.Linq; using Data.Models.Client; using SharedLibraryCore.Interfaces; -using InteractionCallback = System.Func>; -using ScriptInteractionCallback = System.Func>; +using InteractionCallback = System.Func, System.Threading.CancellationToken, System.Threading.Tasks.Task>; namespace SharedLibraryCore.Helpers; diff --git a/SharedLibraryCore/Interfaces/IInteractionData.cs b/SharedLibraryCore/Interfaces/IInteractionData.cs index 9bf10bcbb..aa92b2c4d 100644 --- a/SharedLibraryCore/Interfaces/IInteractionData.cs +++ b/SharedLibraryCore/Interfaces/IInteractionData.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; using Data.Models.Client; -using InteractionCallback = System.Func>; -using ScriptInteractionCallback = System.Func>; +using InteractionCallback = System.Func, System.Threading.CancellationToken, System.Threading.Tasks.Task>; namespace SharedLibraryCore.Interfaces; diff --git a/SharedLibraryCore/Interfaces/IInteractionRegistration.cs b/SharedLibraryCore/Interfaces/IInteractionRegistration.cs index 8a941aa50..d24b3ad36 100644 --- a/SharedLibraryCore/Interfaces/IInteractionRegistration.cs +++ b/SharedLibraryCore/Interfaces/IInteractionRegistration.cs @@ -13,5 +13,5 @@ public interface IInteractionRegistration void UnregisterInteraction(string interactionName); Task> GetInteractions(int? clientId = null, Reference.Game? game = null, CancellationToken token = default); - Task ProcessInteraction(string interactionId, int? clientId = null, Reference.Game? game = null, CancellationToken token = default); + Task ProcessInteraction(string interactionId, int originId, int? targetId = null, Reference.Game? game = null, IDictionary meta = null, CancellationToken token = default); } diff --git a/SharedLibraryCore/Interfaces/IRemoteCommandService.cs b/SharedLibraryCore/Interfaces/IRemoteCommandService.cs new file mode 100644 index 000000000..8099cd851 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IRemoteCommandService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using SharedLibraryCore.Dtos; + +namespace SharedLibraryCore.Interfaces; + +public interface IRemoteCommandService +{ + Task> Execute(int originId, int? targetId, string command, IEnumerable arguments, Server server); +} diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 227635866..cead8dcb2 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -4,7 +4,7 @@ Library net6.0 RaidMax.IW4MAdmin.SharedLibraryCore - 2022.10.12.1 + 2022.10.12.2 RaidMax Forever None Debug;Release;Prerelease @@ -19,7 +19,7 @@ true MIT Shared Library for IW4MAdmin - 2022.10.12.1 + 2022.10.12.2 true $(NoWarn);1591 diff --git a/WebfrontCore/Controllers/ActionController.cs b/WebfrontCore/Controllers/ActionController.cs index cb480787e..677e904c0 100644 --- a/WebfrontCore/Controllers/ActionController.cs +++ b/WebfrontCore/Controllers/ActionController.cs @@ -26,6 +26,7 @@ namespace WebfrontCore.Controllers private readonly ApplicationConfiguration _appConfig; private readonly IMetaServiceV2 _metaService; private readonly IInteractionRegistration _interactionRegistration; + private readonly IRemoteCommandService _remoteCommandService; private readonly string _banCommandName; private readonly string _tempbanCommandName; private readonly string _unbanCommandName; @@ -40,11 +41,12 @@ namespace WebfrontCore.Controllers public ActionController(IManager manager, IEnumerable registeredCommands, ApplicationConfiguration appConfig, IMetaServiceV2 metaService, - IInteractionRegistration interactionRegistration) : base(manager) + IInteractionRegistration interactionRegistration, IRemoteCommandService remoteCommandService) : base(manager) { _appConfig = appConfig; _metaService = metaService; _interactionRegistration = interactionRegistration; + _remoteCommandService = remoteCommandService; foreach (var cmd in registeredCommands) { @@ -104,6 +106,20 @@ namespace WebfrontCore.Controllers metaDict.TryGetValue(nameof(ActionInfo.ShouldRefresh), out var refresh); metaDict.TryGetValue("Data", out var data); metaDict.TryGetValue("InteractionId", out var interactionId); + metaDict.TryGetValue("Inputs", out var template); + + List additionalInputs = null; + var inputKeys = string.Empty; + + if (!string.IsNullOrWhiteSpace(template)) + { + additionalInputs = JsonSerializer.Deserialize>(template); + } + + if (additionalInputs is not null) + { + inputKeys = string.Join(",", additionalInputs.Select(input => input.Name)); + } bool.TryParse(refresh, out var shouldRefresh); @@ -126,9 +142,20 @@ namespace WebfrontCore.Controllers Name = "TargetId", Value = id?.ToString(), Type = "hidden" + }, + new() + { + Name = "CustomInputKeys", + Value = inputKeys, + Type = "hidden" } }; + if (additionalInputs?.Any() ?? false) + { + inputs.AddRange(additionalInputs); + } + var info = new ActionInfo { ActionButtonLabel = label, @@ -141,28 +168,51 @@ namespace WebfrontCore.Controllers return View("_ActionForm", info); } - public async Task DynamicActionAsync(string interactionId, string data, int? targetId, - CancellationToken token = default) + public async Task DynamicActionAsync(CancellationToken token = default) { - if (interactionId == "command") - { - var server = Manager.GetServers().First(); + HttpContext.Request.Query.TryGetValue("InteractionId", out var interactionId); + HttpContext.Request.Query.TryGetValue("CustomInputKeys", out var inputKeys); + HttpContext.Request.Query.TryGetValue("Data", out var data); + HttpContext.Request.Query.TryGetValue("TargetId", out var targetIdString); - return await Task.FromResult(RedirectToAction("Execute", "Console", new + var inputs = new Dictionary(); + + if (!string.IsNullOrWhiteSpace(inputKeys.ToString())) + { + foreach (var key in inputKeys.ToString().Split(",")) { - serverId = server.EndPoint, - command = $"{_appConfig.CommandPrefix}{data}" - })); + HttpContext.Request.Query.TryGetValue(key, out var input); + + if (string.IsNullOrWhiteSpace(input)) + { + continue; + } + + inputs.Add(key, HttpContext.Request.Query[key]); + } } var game = (Reference.Game?)null; + var targetId = (int?)null; + + if (int.TryParse(targetIdString.ToString().Split(",").Last(), out var parsedTargetId)) + { + targetId = parsedTargetId; + } if (targetId.HasValue) { game = (await Manager.GetClientService().Get(targetId.Value))?.GameName; } - return Ok(await _interactionRegistration.ProcessInteraction(interactionId, targetId, game, token)); + if (interactionId.ToString() != "command") + { + return Ok(await _interactionRegistration.ProcessInteraction(interactionId, Client.ClientId, targetId, game, inputs, + token)); + } + + var server = Manager.GetServers().First(); + return Ok(await _remoteCommandService.Execute(Client.ClientId, targetId, data, inputs.Values.Select(input => input), server)); } public IActionResult BanForm() diff --git a/WebfrontCore/Controllers/ConsoleController.cs b/WebfrontCore/Controllers/ConsoleController.cs index 1221b16fc..75d606e0c 100644 --- a/WebfrontCore/Controllers/ConsoleController.cs +++ b/WebfrontCore/Controllers/ConsoleController.cs @@ -1,22 +1,19 @@ using Microsoft.AspNetCore.Mvc; using SharedLibraryCore; -using SharedLibraryCore.Configuration; -using SharedLibraryCore.Database.Models; using SharedLibraryCore.Dtos; using SharedLibraryCore.Interfaces; using System.Linq; using System.Threading.Tasks; -using Data.Models; namespace WebfrontCore.Controllers { public class ConsoleController : BaseController { - private readonly ApplicationConfiguration _appconfig; + private readonly IRemoteCommandService _remoteCommandService; - public ConsoleController(IManager manager) : base(manager) + public ConsoleController(IManager manager, IRemoteCommandService remoteCommandService) : base(manager) { - _appconfig = manager.GetApplicationSettings().Configuration(); + _remoteCommandService = remoteCommandService; } public IActionResult Index() @@ -37,75 +34,8 @@ namespace WebfrontCore.Controllers public async Task Execute(long serverId, string command) { var server = Manager.GetServers().First(s => s.EndPoint == serverId); - - var client = new EFClient - { - ClientId = Client.ClientId, - Level = Client.Level, - NetworkId = Client.NetworkId, - CurrentServer = server, - CurrentAlias = new EFAlias() - { - Name = Client.Name - } - }; - - var remoteEvent = new GameEvent - { - Type = GameEvent.EventType.Command, - Data = command.StartsWith(_appconfig.CommandPrefix) || - command.StartsWith(_appconfig.BroadcastCommandPrefix) - ? command - : $"{_appconfig.CommandPrefix}{command}", - Origin = client, - Owner = server, - IsRemote = true - }; - - Manager.AddEvent(remoteEvent); - CommandResponseInfo[] response = null; - - try - { - // wait for the event to process - var completedEvent = - await remoteEvent.WaitAsync(Utilities.DefaultCommandTimeout, server.Manager.CancellationToken); - - if (completedEvent.FailReason == GameEvent.EventFailReason.Timeout) - { - response = new[] - { - new CommandResponseInfo() - { - ClientId = client.ClientId, - Response = Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"] - } - }; - } - - else - { - response = completedEvent.Output.Select(output => new CommandResponseInfo() - { - Response = output, - ClientId = client.ClientId - }).ToArray(); - } - } - - catch (System.OperationCanceledException) - { - response = new[] - { - new CommandResponseInfo - { - ClientId = client.ClientId, - Response = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RESTART_SUCCESS"] - } - }; - } - - return remoteEvent.Failed ? StatusCode(400, response) : Ok(response); + var response = await _remoteCommandService.Execute(Client.ClientId, null, command, Enumerable.Empty(), server); + return response.Any() ? StatusCode(400, response) : Ok(response); } } } diff --git a/WebfrontCore/Startup.cs b/WebfrontCore/Startup.cs index 9fce5a34c..fc49e5724 100644 --- a/WebfrontCore/Startup.cs +++ b/WebfrontCore/Startup.cs @@ -141,6 +141,7 @@ namespace WebfrontCore .GetRequiredService()); services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService()); services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService()); + services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/WebfrontCore/ViewModels/InputInfo.cs b/WebfrontCore/ViewModels/InputInfo.cs index bf0227eb5..686bb1135 100644 --- a/WebfrontCore/ViewModels/InputInfo.cs +++ b/WebfrontCore/ViewModels/InputInfo.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Collections.Generic; namespace WebfrontCore.ViewModels {