diff --git a/Application/API/Master/IMasterApi.cs b/Application/API/Master/IMasterApi.cs
index afac522c5..f6a46bf21 100644
--- a/Application/API/Master/IMasterApi.cs
+++ b/Application/API/Master/IMasterApi.cs
@@ -1,5 +1,7 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
+using IW4MAdmin.Application.Helpers;
using Newtonsoft.Json;
using RestEase;
using SharedLibraryCore.Helpers;
@@ -35,6 +37,13 @@ namespace IW4MAdmin.Application.API.Master
public string Message { get; set; }
}
+ public class PluginSubscriptionContent
+ {
+ public string Content { get; set; }
+ public PluginType Type { get; set; }
+ }
+
+
///
/// Defines the capabilities of the master API
///
@@ -63,5 +72,8 @@ namespace IW4MAdmin.Application.API.Master
[Get("localization/{languageTag}")]
Task GetLocalization([Path("languageTag")] string languageTag);
+
+ [Get("plugin_subscriptions")]
+ Task> GetPluginSubscription([Query("instance_id")] Guid instanceId, [Query("subscription_id")] string subscription_id);
}
}
diff --git a/Application/Main.cs b/Application/Main.cs
index 9cdce81e5..86732d3cd 100644
--- a/Application/Main.cs
+++ b/Application/Main.cs
@@ -135,7 +135,7 @@ namespace IW4MAdmin.Application
await ApplicationTask;
}
- catch (Exception e)
+ catch (Exception e)
{
string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
Console.WriteLine($"{failMessage}: {e.GetExceptionInfo()}");
@@ -226,12 +226,17 @@ namespace IW4MAdmin.Application
///
private static IServiceCollection ConfigureServices(string[] args)
{
+ var appConfigHandler = new BaseConfigurationHandler("IW4MAdminSettings");
+ var appConfig = appConfigHandler.Configuration();
var defaultLogger = new Logger("IW4MAdmin-Manager");
- var pluginImporter = new PluginImporter(defaultLogger);
+
+ var masterUri = Utilities.IsDevelopment ? new Uri("http://127.0.0.1:8080") : appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl;
+ var apiClient = RestClient.For(masterUri);
+ var pluginImporter = new PluginImporter(defaultLogger, appConfig, apiClient, new RemoteAssemblyHandler(defaultLogger, appConfig));
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(_serviceProvider => serviceCollection)
- .AddSingleton(new BaseConfigurationHandler("IW4MAdminSettings") as IConfigurationHandler)
+ .AddSingleton(appConfigHandler as IConfigurationHandler)
.AddSingleton(new BaseConfigurationHandler("CommandConfiguration") as IConfigurationHandler)
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService>().Configuration() ?? new ApplicationConfiguration())
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService>().Configuration() ?? new CommandConfiguration())
@@ -255,19 +260,17 @@ namespace IW4MAdmin.Application
.AddSingleton, UpdatedAliasResourceQueryHelper>()
.AddSingleton, ChatResourceQueryHelper>()
.AddTransient()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton(apiClient)
.AddSingleton(_serviceProvider =>
{
var config = _serviceProvider.GetRequiredService>().Configuration();
return Localization.Configure.Initialize(useLocalTranslation: config?.UseLocalTranslations ?? false,
apiInstance: _serviceProvider.GetRequiredService(),
customLocale: config?.EnableCustomLocale ?? false ? (config.CustomLocale ?? "en-US") : "en-US");
- })
- .AddSingleton()
- .AddSingleton(_serviceProvider => RestClient
- .For(Utilities.IsDevelopment ? new Uri("http://127.0.0.1:8080") : _serviceProvider
- .GetRequiredService>().Configuration()?.MasterUrl ??
- new ApplicationConfiguration().MasterUrl))
- .AddSingleton();
+ });
if (args.Contains("serialevents"))
{
diff --git a/Application/Misc/PluginImporter.cs b/Application/Misc/PluginImporter.cs
index 828333976..29723eba4 100644
--- a/Application/Misc/PluginImporter.cs
+++ b/Application/Misc/PluginImporter.cs
@@ -6,6 +6,8 @@ using SharedLibraryCore.Interfaces;
using System.Linq;
using SharedLibraryCore;
using IW4MAdmin.Application.Misc;
+using IW4MAdmin.Application.API.Master;
+using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Helpers
{
@@ -15,12 +17,19 @@ namespace IW4MAdmin.Application.Helpers
///
public class PluginImporter : IPluginImporter
{
+ private IEnumerable _pluginSubscription;
private static readonly string PLUGIN_DIR = "Plugins";
private readonly ILogger _logger;
+ private readonly IRemoteAssemblyHandler _remoteAssemblyHandler;
+ private readonly IMasterApi _masterApi;
+ private readonly ApplicationConfiguration _appConfig;
- public PluginImporter(ILogger logger)
+ public PluginImporter(ILogger logger, ApplicationConfiguration appConfig, IMasterApi masterApi, IRemoteAssemblyHandler remoteAssemblyHandler)
{
_logger = logger;
+ _masterApi = masterApi;
+ _remoteAssemblyHandler = remoteAssemblyHandler;
+ _appConfig = appConfig;
}
///
@@ -33,11 +42,11 @@ namespace IW4MAdmin.Application.Helpers
if (Directory.Exists(pluginDir))
{
- string[] scriptPluginFiles = Directory.GetFiles(pluginDir, "*.js");
+ var scriptPluginFiles = Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts());
- _logger.WriteInfo($"Discovered {scriptPluginFiles.Length} potential script plugins");
+ _logger.WriteInfo($"Discovered {scriptPluginFiles.Count()} potential script plugins");
- if (scriptPluginFiles.Length > 0)
+ if (scriptPluginFiles.Count() > 0)
{
foreach (string fileName in scriptPluginFiles)
{
@@ -66,7 +75,10 @@ namespace IW4MAdmin.Application.Helpers
if (dllFileNames.Length > 0)
{
- var assemblies = dllFileNames.Select(_name => Assembly.LoadFrom(_name));
+ // we only want to load the most recent assembly in case of duplicates
+ var assemblies = dllFileNames.Select(_name => Assembly.LoadFrom(_name))
+ .Union(GetRemoteAssemblies())
+ .GroupBy(_assembly => _assembly.FullName).Select(_assembly => _assembly.OrderByDescending(_assembly => _assembly.GetName().Version).First());
pluginTypes = assemblies
.SelectMany(_asm => _asm.GetTypes())
@@ -84,5 +96,47 @@ namespace IW4MAdmin.Application.Helpers
return (pluginTypes, commandTypes);
}
+
+ private IEnumerable GetRemoteAssemblies()
+ {
+ try
+ {
+ if (_pluginSubscription == null)
+ _pluginSubscription = _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
+
+ return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription.Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray());
+ }
+
+ catch (Exception ex)
+ {
+ _logger.WriteWarning("Could not load remote assemblies");
+ _logger.WriteDebug(ex.GetExceptionInfo());
+ return Enumerable.Empty();
+ }
+ }
+
+ private IEnumerable GetRemoteScripts()
+ {
+ try
+ {
+ if (_pluginSubscription == null)
+ _pluginSubscription = _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
+
+ return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription.Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray());
+ }
+
+ catch (Exception ex)
+ {
+ _logger.WriteWarning("Could not load remote assemblies");
+ _logger.WriteDebug(ex.GetExceptionInfo());
+ return Enumerable.Empty();
+ }
+ }
+ }
+
+ public enum PluginType
+ {
+ Binary,
+ Script
}
}
diff --git a/Application/Misc/RemoteAssemblyHandler.cs b/Application/Misc/RemoteAssemblyHandler.cs
new file mode 100644
index 000000000..1733495c1
--- /dev/null
+++ b/Application/Misc/RemoteAssemblyHandler.cs
@@ -0,0 +1,76 @@
+using SharedLibraryCore;
+using SharedLibraryCore.Configuration;
+using SharedLibraryCore.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace IW4MAdmin.Application.Misc
+{
+ public class RemoteAssemblyHandler : IRemoteAssemblyHandler
+ {
+ private const int keyLength = 32;
+ private const int tagLength = 16;
+ private const int nonceLength = 12;
+ private const int iterationCount = 10000;
+
+ private readonly ApplicationConfiguration _appconfig;
+ private readonly ILogger _logger;
+
+ public RemoteAssemblyHandler(ILogger logger, ApplicationConfiguration appconfig)
+ {
+ _appconfig = appconfig;
+ _logger = logger;
+ }
+
+ public IEnumerable DecryptAssemblies(string[] encryptedAssemblies)
+ {
+ return DecryptContent(encryptedAssemblies)
+ .Select(decryptedAssembly => Assembly.Load(decryptedAssembly));
+ }
+
+ public IEnumerable DecryptScripts(string[] encryptedScripts)
+ {
+ return DecryptContent(encryptedScripts).Select(decryptedScript => Encoding.UTF8.GetString(decryptedScript));
+ }
+
+ private byte[][] DecryptContent(string[] content)
+ {
+ if (string.IsNullOrEmpty(_appconfig.Id) || string.IsNullOrWhiteSpace(_appconfig.SubscriptionId))
+ {
+ _logger.WriteWarning($"{nameof(_appconfig.Id)} and {nameof(_appconfig.SubscriptionId)} must be provided to attempt loading remote assemblies/scripts");
+ return new byte[0][];
+ }
+
+ var assemblies = content.Select(piece =>
+ {
+ byte[] byteContent = Convert.FromBase64String(piece);
+ byte[] encryptedContent = byteContent.Take(byteContent.Length - (tagLength + nonceLength)).ToArray();
+ byte[] tag = byteContent.Skip(byteContent.Length - (tagLength + nonceLength)).Take(tagLength).ToArray();
+ byte[] nonce = byteContent.Skip(byteContent.Length - nonceLength).Take(nonceLength).ToArray();
+ byte[] decryptedContent = new byte[encryptedContent.Length];
+
+ var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id.ToString()), iterationCount, HashAlgorithmName.SHA512);
+ var encryption = new AesGcm(keyGen.GetBytes(keyLength));
+
+ try
+ {
+ encryption.Decrypt(nonce, encryptedContent, tag, decryptedContent);
+ }
+
+ catch (CryptographicException ex)
+ {
+ _logger.WriteError("Could not obtain remote plugin assemblies");
+ _logger.WriteDebug(ex.GetExceptionInfo());
+ }
+
+ return decryptedContent;
+ });
+
+ return assemblies.ToArray();
+ }
+ }
+}
diff --git a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs
index c2cf6491e..4764366f7 100644
--- a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs
+++ b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs
@@ -99,6 +99,8 @@ namespace SharedLibraryCore.Configuration
[ConfigurationIgnore]
public string Id { get; set; }
[ConfigurationIgnore]
+ public string SubscriptionId { get; set; }
+ [ConfigurationIgnore]
public MapConfiguration[] Maps { get; set; }
[ConfigurationIgnore]
public QuickMessageConfiguration[] QuickMessages { get; set; }
diff --git a/SharedLibraryCore/Interfaces/IRemoteAssemblyHandler.cs b/SharedLibraryCore/Interfaces/IRemoteAssemblyHandler.cs
new file mode 100644
index 000000000..278d93d96
--- /dev/null
+++ b/SharedLibraryCore/Interfaces/IRemoteAssemblyHandler.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace SharedLibraryCore.Interfaces
+{
+ public interface IRemoteAssemblyHandler
+ {
+ IEnumerable DecryptAssemblies(string[] encryptedAssemblies);
+ IEnumerable DecryptScripts(string[] encryptedScripts);
+ }
+}