add support for plugin generated pages (interactions). add disallow vpn command

This commit is contained in:
RaidMax 2022-10-17 09:17:43 -05:00
parent 3295315339
commit 3367c5c22f
17 changed files with 311 additions and 45 deletions

View File

@ -24,7 +24,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2038" />
<PackageReference Include="Jint" Version="3.0.0-beta-2041" />
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
<PrivateAssets>all</PrivateAssets>

View File

@ -85,7 +85,7 @@ namespace IW4MAdmin.Application
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
@ -115,9 +115,11 @@ namespace IW4MAdmin.Application
_changeHistoryService = changeHistoryService;
_appConfig = appConfig;
Plugins = plugins;
InteractionRegistration = interactionRegistration;
}
public IEnumerable<IPlugin> Plugins { get; }
public IInteractionRegistration InteractionRegistration { get; }
public async Task ExecuteEvent(GameEvent newEvent)
{

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
namespace IW4MAdmin.Application.Extensions;
public static class ScriptPluginExtensions
{
public static IEnumerable<object> GetClientsBasicData(
this DbSet<Data.Models.Client.EFClient> set, int[] clientIds)
{
return set.Where(client => clientIds.Contains(client.ClientId))
.Select(client => new
{
client.ClientId,
client.CurrentAlias,
client.Level,
client.NetworkId
}).ToList();
}
}

View File

@ -70,23 +70,11 @@ public class InteractionRegistration : IInteractionRegistration
}
}
public async Task<IEnumerable<IInteractionData>> GetInteractions(int? clientId = null,
public async Task<IEnumerable<IInteractionData>> GetInteractions(string interactionPrefix = null,
int? clientId = null,
Reference.Game? game = null, CancellationToken token = default)
{
return (await Task.WhenAll(_interactions.Select(async kvp =>
{
try
{
return await kvp.Value(clientId, game, token);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not get interaction for interaction {InteractionName} and ClientId {ClientId}", kvp.Key,
clientId);
return null;
}
}))).Where(interaction => interaction is not null);
return await GetInteractionsPrivate(interactionPrefix, clientId, game, token);
}
public async Task<string> ProcessInteraction(string interactionId, int originId, int? targetId = null,
@ -115,17 +103,40 @@ public class InteractionRegistration : IInteractionRegistration
continue;
}
return scriptPlugin.ExecuteAction<string>(interaction.ScriptAction, originId, targetId, game, meta, token);
return scriptPlugin.ExecuteAction<string>(interaction.ScriptAction, originId, targetId, game, meta,
token);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not process interaction for interaction {InteractionName} and OriginId {ClientId}",
"Could not process interaction for {InteractionName} and OriginId {ClientId}",
interactionId, originId);
}
return null;
}
private async Task<IEnumerable<IInteractionData>> GetInteractionsPrivate(string prefix = null, int? clientId = null,
Reference.Game? game = null, CancellationToken token = default)
{
return (await Task.WhenAll(_interactions
.Where(interaction => string.IsNullOrWhiteSpace(prefix) || interaction.Key.StartsWith(prefix)).Select(
async kvp =>
{
try
{
return await kvp.Value(clientId, game, token);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not get interaction for {InteractionName} and ClientId {ClientId}",
kvp.Key,
clientId);
return null;
}
}))).Where(interaction => interaction is not null);
}
}

View File

@ -13,6 +13,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using IW4MAdmin.Application.Extensions;
using Jint.Runtime.Interop;
using Microsoft.Extensions.Logging;
using Serilog.Context;
@ -112,7 +113,7 @@ namespace IW4MAdmin.Application.Misc
}
_scriptEngine = new Engine(cfg =>
cfg.AddExtensionMethods(typeof(Utilities), typeof(Enumerable), typeof(Queryable))
cfg.AddExtensionMethods(typeof(Utilities), typeof(Enumerable), typeof(Queryable), typeof(ScriptPluginExtensions))
.AllowClr(new[]
{
typeof(System.Net.Http.HttpClient).Assembly,

View File

@ -22,7 +22,7 @@ public class Plugin : IPlugin
private static readonly string[] DisabledCommands = {nameof(PrivateMessageAdminsCommand), "PrivateMessageCommand"};
private readonly IInteractionRegistration _interactionRegistration;
private readonly IRemoteCommandService _remoteCommandService;
private static readonly string MuteInteraction = nameof(MuteInteraction);
private static readonly string MuteInteraction = "Webfront::Profile::Mute";
public Plugin(ILogger<Plugin> logger, IMetaServiceV2 metaService, IInteractionRegistration interactionRegistration,
ITranslationLookup translationLookup, IRemoteCommandService remoteCommandService)

View File

@ -1,4 +1,7 @@
let vpnExceptionIds = [];
const vpnAllowListKey = 'Webfront::Nav::Admin::VPNAllowList';
const vpnWhitelistKey = 'Webfront::Profile::VPNWhitelist';
const commands = [{
name: 'whitelistvpn',
description: 'whitelists a player\'s client id from VPN detection',
@ -15,8 +18,35 @@ const commands = [{
gameEvent.Origin.Tell(`Successfully whitelisted ${gameEvent.Target.Name}`);
}
},
{
name: 'disallowvpn',
description: 'disallows a player from connecting with a VPN',
alias: 'dv',
permission: 'SeniorAdmin',
targetRequired: true,
arguments: [{
name: 'player',
required: true
}],
execute: (gameEvent) => {
vpnExceptionIds = vpnExceptionIds.filter(exception => parseInt(exception) !== parseInt(gameEvent.Target.ClientId));
plugin.configHandler.SetValue('vpnExceptionIds', vpnExceptionIds);
gameEvent.Origin.Tell(`Successfully disallowed ${gameEvent.Target.Name} from connecting with VPN`);
}
}];
const getClientsData = (clientIds) => {
const contextFactory = _serviceResolver.ResolveService('IDatabaseContextFactory');
const context = contextFactory.CreateContext(false);
const clientSet = context.Clients;
const clients = clientSet.GetClientsBasicData(clientIds);
context.Dispose();
return clients;
}
const plugin = {
author: 'RaidMax',
version: 1.5,
@ -28,7 +58,7 @@ const plugin = {
let exempt = false;
// prevent players that are exempt from being kicked
vpnExceptionIds.forEach(function (id) {
if (id == origin.ClientId) { // when loaded from the config the "id" type is not the same as the ClientId type
if (parseInt(id) === parseInt(origin.ClientId)) {
exempt = true;
return false;
}
@ -83,33 +113,99 @@ const plugin = {
this.logger.WriteInfo(`Loaded ${vpnExceptionIds.length} ids into whitelist`);
this.interactionRegistration = _serviceResolver.ResolveService('IInteractionRegistration');
this.interactionRegistration.RegisterScriptInteraction('WhitelistVPN', this.name, (targetId, game, token) => {
if (vpnExceptionIds.includes(targetId)) {
return;
}
// registers the profile action
this.interactionRegistration.RegisterScriptInteraction(vpnWhitelistKey, this.name, (targetId, game, token) => {
const helpers = importNamespace('SharedLibraryCore.Helpers');
const interactionData = new helpers.InteractionData();
interactionData.EntityId = targetId;
interactionData.Name = 'Whitelist VPN';
interactionData.DisplayMeta = 'oi-circle-check';
interactionData.ActionMeta.Add('InteractionId', 'command');
interactionData.ActionMeta.Add('Data', `whitelistvpn`);
interactionData.ActionMeta.Add('ActionButtonLabel', 'Allow');
interactionData.ActionMeta.Add('Name', 'Allow VPN Connection');
interactionData.ActionMeta.Add('ShouldRefresh', true.toString());
interactionData.ActionPath = 'DynamicAction';
interactionData.InteractionId = vpnWhitelistKey;
interactionData.EntityId = targetId;
interactionData.MinimumPermission = 3;
interactionData.Source = this.name;
interactionData.ActionMeta.Add('InteractionId', 'command'); // indicate we're wanting to execute a command
interactionData.ActionMeta.Add('ShouldRefresh', true.toString()); // indicates that the page should refresh after performing the action
if (vpnExceptionIds.includes(targetId)) {
interactionData.Name = _localization.LocalizationIndex['WEBFRONT_VPN_BUTTON_DISALLOW']; // text for the profile button
interactionData.DisplayMeta = 'oi-circle-x';
interactionData.ActionMeta.Add('Data', `disallowvpn`); // command to execute
interactionData.ActionMeta.Add('ActionButtonLabel', _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_DISALLOW_CONFIRM']); // confirm button on the dialog
interactionData.ActionMeta.Add('Name', _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_DISALLOW_TITLE']); // title on the confirm dialog
} else {
interactionData.Name = _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_ALLOW']; // text for the profile button
interactionData.DisplayMeta = 'oi-circle-check';
interactionData.ActionMeta.Add('Data', `whitelistvpn`); // command to execute
interactionData.ActionMeta.Add('ActionButtonLabel', _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_ALLOW_CONFIRM']); // confirm button on the dialog
interactionData.ActionMeta.Add('Name', _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_ALLOW_TITLE']); // title on the confirm dialog
}
return interactionData;
});
// registers the navigation/page
this.interactionRegistration.RegisterScriptInteraction(vpnAllowListKey, this.name, (targetId, game, token) => {
const helpers = importNamespace('SharedLibraryCore.Helpers');
const interactionData = new helpers.InteractionData();
interactionData.Name = _localization.LocalizationIndex['WEBFRONT_NAV_VPN_TITLE']; // navigation link name
interactionData.Description = _localization.LocalizationIndex['WEBFRONT_NAV_VPN_DESC']; // alt and title
interactionData.DisplayMeta = 'oi-circle-check'; // nav icon
interactionData.InteractionId = vpnAllowListKey;
interactionData.MinimumPermission = 3; // moderator
interactionData.InteractionType = 2; // 1 is RawContent for apis etc..., 2 is
interactionData.Source = this.name;
interactionData.ScriptAction = (sourceId, targetId, game, meta, token) => {
const clientsData = getClientsData(vpnExceptionIds);
let table = '<table class="table bg-dark-dm bg-light-lm">';
const disallowInteraction = {
InteractionId: 'command',
Data: 'disallowvpn',
ActionButtonLabel: _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_DISALLOW_CONFIRM'],
Name: _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_DISALLOW_TITLE']
};
if (clientsData.length === 0)
{
table += `<tr><td>No players are whitelisted.</td></tr>`
}
clientsData.forEach(client => {
table += `<tr>
<td>
<a href="/Client/Profile/${client.ClientId}" class="level-color-${client.Level.toLowerCase()} no-decoration">${client.CurrentAlias.Name.StripColors()}</a>
</td>
<td>
<a href="#" class="profile-action no-decoration float-right" data-action="DynamicAction" data-action-id="${client.ClientId}"
data-action-meta="${encodeURI(JSON.stringify(disallowInteraction))}">
<div class="btn">
<i class="oi oi-circle-x mr-5 font-size-12"></i>
<span class="text-truncate">${_localization.LocalizationIndex['WEBFRONT_VPN_BUTTON_DISALLOW']}</span>
</div>
</a>
</td>
</tr>`;
});
table += '</table>';
return table;
}
return interactionData;
});
},
onUnloadAsync: function () {
this.interactionRegistration.UnregisterInteraction('WhitelistVPN');
this.interactionRegistration.UnregisterInteraction(vpnWhitelistKey);
this.interactionRegistration.UnregisterInteraction(vpnAllowListKey);
},
onTickAsync: function (server) {

View File

@ -19,6 +19,7 @@ namespace SharedLibraryCore
{
public class BaseController : Controller
{
protected readonly IInteractionRegistration InteractionRegistration;
protected readonly IAlertManager AlertManager;
/// <summary>
@ -41,6 +42,7 @@ namespace SharedLibraryCore
public BaseController(IManager manager)
{
InteractionRegistration = manager.InteractionRegistration;
AlertManager = manager.AlertManager;
Manager = manager;
Localization = Utilities.CurrentLocalization.LocalizationIndex;
@ -71,9 +73,7 @@ namespace SharedLibraryCore
CurrentAlias = new EFAlias { Name = "Webfront Guest" }
};
}
protected async Task SignInAsync(ClaimsPrincipal claimsPrinciple)
{
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrinciple,
@ -86,7 +86,7 @@ namespace SharedLibraryCore
});
}
public override void OnActionExecuting(ActionExecutingContext context)
public override async void OnActionExecuting(ActionExecutingContext context)
{
if (!HttpContext.Connection.RemoteIpAddress.GetAddressBytes().SequenceEqual(LocalHost))
{
@ -154,6 +154,7 @@ namespace SharedLibraryCore
&& !communityName.Contains("IW4MAdmin")
&& AppConfig.CommunityInformation.IsEnabled;
ViewBag.Interactions = await InteractionRegistration.GetInteractions("Webfront::Nav");
ViewBag.Authorized = Authorized;
ViewBag.Url = AppConfig.WebfrontUrl;
ViewBag.User = Client;

View File

@ -10,6 +10,8 @@ namespace SharedLibraryCore.Helpers;
public class InteractionData : IInteractionData
{
public int? EntityId { get; set; }
public string InteractionId { get; set; }
public InteractionType InteractionType { get; set; }
public bool Enabled { get; set; }
public string Name { get; set; }
public string Description { get; set; }

View File

@ -8,6 +8,8 @@ namespace SharedLibraryCore.Interfaces;
public interface IInteractionData
{
int? EntityId { get; }
string InteractionId { get; }
InteractionType InteractionType { get; }
bool Enabled { get; }
string Name { get; }
string Description { get; }
@ -22,3 +24,10 @@ public interface IInteractionData
InteractionCallback Action { get; }
Delegate ScriptAction { get; }
}
public enum InteractionType
{
ActionButton,
RawContent,
TemplateContent
}

View File

@ -11,7 +11,7 @@ public interface IInteractionRegistration
void RegisterScriptInteraction(string interactionName, string source, Delegate interactionRegistration);
void RegisterInteraction(string interactionName, Func<int?, Reference.Game?, CancellationToken, Task<IInteractionData>> interactionRegistration);
void UnregisterInteraction(string interactionName);
Task<IEnumerable<IInteractionData>> GetInteractions(int? clientId = null,
Task<IEnumerable<IInteractionData>> GetInteractions(string interactionPrefix = null, int? clientId = null,
Reference.Game? game = null, CancellationToken token = default);
Task<string> ProcessInteraction(string interactionId, int originId, int? targetId = null, Reference.Game? game = null, IDictionary<string, string> meta = null, CancellationToken token = default);
}

View File

@ -105,5 +105,6 @@ namespace SharedLibraryCore.Interfaces
event EventHandler<GameEvent> OnGameEventExecuted;
IAlertManager AlertManager { get; }
IInteractionRegistration InteractionRegistration { get; }
}
}

View File

@ -27,6 +27,7 @@ namespace WebfrontCore.Controllers
private readonly IMetaServiceV2 _metaService;
private readonly IInteractionRegistration _interactionRegistration;
private readonly IRemoteCommandService _remoteCommandService;
private readonly ITranslationLookup _translationLookup;
private readonly string _banCommandName;
private readonly string _tempbanCommandName;
private readonly string _unbanCommandName;
@ -41,12 +42,14 @@ namespace WebfrontCore.Controllers
public ActionController(IManager manager, IEnumerable<IManagerCommand> registeredCommands,
ApplicationConfiguration appConfig, IMetaServiceV2 metaService,
IInteractionRegistration interactionRegistration, IRemoteCommandService remoteCommandService) : base(manager)
IInteractionRegistration interactionRegistration, IRemoteCommandService remoteCommandService,
ITranslationLookup translationLookup) : base(manager)
{
_appConfig = appConfig;
_metaService = metaService;
_interactionRegistration = interactionRegistration;
_remoteCommandService = remoteCommandService;
_translationLookup = translationLookup;
foreach (var cmd in registeredCommands)
{
@ -94,7 +97,18 @@ namespace WebfrontCore.Controllers
public IActionResult DynamicActionForm(int? id, string meta)
{
var metaDict = JsonSerializer.Deserialize<Dictionary<string, string>>(meta);
if (Client.ClientId < 1)
{
return Ok(new[]
{
new CommandResponseInfo
{
Response = _translationLookup["SERVER_COMMANDS_INTERCEPTED"]
}
});
}
var metaDict = JsonSerializer.Deserialize<Dictionary<string, string>>(meta.TrimEnd('"').TrimStart('"'));
if (metaDict is null)
{
@ -170,6 +184,17 @@ namespace WebfrontCore.Controllers
public async Task<IActionResult> DynamicActionAsync(CancellationToken token = default)
{
if (Client.ClientId < 1)
{
return Ok(new[]
{
new CommandResponseInfo
{
Response = _translationLookup["SERVER_COMMANDS_INTERCEPTED"]
}
});
}
HttpContext.Request.Query.TryGetValue("InteractionId", out var interactionId);
HttpContext.Request.Query.TryGetValue("CustomInputKeys", out var inputKeys);
HttpContext.Request.Query.TryGetValue("Data", out var data);

View File

@ -77,7 +77,8 @@ namespace WebfrontCore.Controllers
note.OriginEntityName = await _clientService.GetClientNameById(note.OriginEntityId);
}
var interactions = await _interactionRegistration.GetInteractions(id, client.GameName, token);
var interactions =
await _interactionRegistration.GetInteractions("Webfront::Profile", id, client.GameName, token);
// even though we haven't set their level to "banned" yet
// (ie they haven't reconnected with the infringing player identifier)

View File

@ -0,0 +1,37 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
namespace WebfrontCore.Controllers;
public class InteractionController : BaseController
{
private readonly IInteractionRegistration _interactionRegistration;
public InteractionController(IManager manager, IInteractionRegistration interactionRegistration) : base(manager)
{
_interactionRegistration = interactionRegistration;
}
[HttpGet("[controller]/[action]/{interactionName}")]
public async Task<IActionResult> Render([FromRoute]string interactionName, CancellationToken token)
{
var interactionData = (await _interactionRegistration.GetInteractions(interactionName, token: token)).FirstOrDefault();
if (interactionData is null)
{
return NotFound();
}
ViewBag.Title = interactionData.Description;
var result = await _interactionRegistration.ProcessInteraction(interactionName, Client.ClientId, token: token);
return interactionData.InteractionType == InteractionType.TemplateContent
? View("Render", result ?? "")
: Ok(result);
}
}

View File

@ -0,0 +1,8 @@
@model string
<div class="content text-wrap mt-20">
<h2 class="content-title">
<color-code value="@ViewBag.Title"></color-code>
</h2>
@Html.Raw(Model)
</div>

View File

@ -1,6 +1,7 @@
@using SharedLibraryCore.Configuration
@using SharedLibraryCore.Dtos
@using Data.Models.Client
@using SharedLibraryCore.Interfaces
<!-- left side navigation -->
<div class="sidebar-overlay" onclick="halfmoon.toggleSidebar()"></div>
@ -43,6 +44,23 @@
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_HELP"]</span>
</a>
</has-permission>
@foreach (IInteractionData interactionData in ViewBag.Interactions)
{
if (!interactionData.InteractionId.StartsWith("Webfront::Nav::Main"))
{
continue;
}
if (ViewBag.User.Level >= interactionData.MinimumPermission)
{
<a asp-controller="Interaction" asp-action="Render" asp-route-interactionName="@interactionData.InteractionId" class="sidebar-link">
<i class="oi @interactionData.DisplayMeta mr-5"></i>
<span class="name">@interactionData.Name</span>
</a>
}
}
<!-- profile -->
<has-permission entity="ProfilePage" required-permission="Read">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@ViewBag.User.ClientId" class="sidebar-link">
@ -104,6 +122,23 @@
</div>
</a>
}
@foreach (IInteractionData interactionData in ViewBag.Interactions)
{
if (!interactionData.InteractionId.StartsWith("Webfront::Nav::Social"))
{
continue;
}
if (ViewBag.User.Level >= interactionData.MinimumPermission)
{
<a asp-controller="Interaction" asp-action="Render" asp-route-interactionName="@interactionData.InteractionId" class="sidebar-link">
<i class="oi @interactionData.DisplayMeta mr-5"></i>
<span class="name">@interactionData.Name</span>
</a>
}
}
<br/>
<!-- admin -->
@ -142,6 +177,22 @@
<span class="name">@ViewBag.Localization["WEBFRONT_ACTION_RECENT_CLIENTS"]</span>
</a>
</has-permission>
@foreach (IInteractionData interactionData in ViewBag.Interactions)
{
if (!interactionData.InteractionId.StartsWith("Webfront::Nav::Admin"))
{
continue;
}
if (ViewBag.User.Level >= interactionData.MinimumPermission)
{
<a asp-controller="Interaction" asp-action="Render" asp-route-interactionName="@interactionData.InteractionId" class="sidebar-link">
<i class="oi @interactionData.DisplayMeta mr-5"></i>
<span class="name">@interactionData.Name</span>
</a>
}
}
@if (ViewBag.Authorized)
{