using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using IW4MAdmin.Application.API.Master; using Microsoft.Extensions.Logging; using SharedLibraryCore; using SharedLibraryCore.Configuration; using SharedLibraryCore.Interfaces; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace IW4MAdmin.Application.Plugin { /// /// implementation of IPluginImporter /// discovers plugins and script plugins /// public class PluginImporter : IPluginImporter { private IEnumerable _pluginSubscription; private const string PluginDir = "Plugins"; private const string PluginV2Match = "^ *((?:var|const|let) +init)|function init"; private readonly ILogger _logger; private readonly IRemoteAssemblyHandler _remoteAssemblyHandler; private readonly IMasterApi _masterApi; private readonly ApplicationConfiguration _appConfig; private static readonly Type[] FilterTypes = { typeof(IPlugin), typeof(IPluginV2), typeof(Command), typeof(IBaseConfiguration) }; public PluginImporter(ILogger logger, ApplicationConfiguration appConfig, IMasterApi masterApi, IRemoteAssemblyHandler remoteAssemblyHandler) { _logger = logger; _masterApi = masterApi; _remoteAssemblyHandler = remoteAssemblyHandler; _appConfig = appConfig; } /// /// discovers all the script plugins in the plugins dir /// /// public IEnumerable<(Type, string)> DiscoverScriptPlugins() { var pluginDir = $"{Utilities.OperatingDirectory}{PluginDir}{Path.DirectorySeparatorChar}"; if (!Directory.Exists(pluginDir)) { return Enumerable.Empty<(Type, string)>(); } var scriptPluginFiles = Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts()).ToList(); var bothVersionPlugins = scriptPluginFiles.Select(fileName => { _logger.LogDebug("Discovered script plugin {FileName}", fileName); try { var fileContents = File.ReadAllLines(fileName); var isValidV2 = fileContents.Any(line => Regex.IsMatch(line, PluginV2Match)); return isValidV2 ? (typeof(IPluginV2), fileName) : (typeof(IPlugin), fileName); } catch { return (typeof(IPlugin), fileName); } }).ToList(); return bothVersionPlugins; } /// /// discovers all the C# assembly plugins and commands /// /// public (IEnumerable, IEnumerable, IEnumerable) DiscoverAssemblyPluginImplementations() { var pluginDir = $"{Utilities.OperatingDirectory}{PluginDir}{Path.DirectorySeparatorChar}"; var pluginTypes = new List(); var commandTypes = new List(); var configurationTypes = new List(); if (!Directory.Exists(pluginDir)) { return (pluginTypes, commandTypes, configurationTypes); } var dllFileNames = Directory.GetFiles(pluginDir, "*.dll"); _logger.LogDebug("Discovered {Count} potential plugin assemblies", dllFileNames.Length); if (!dllFileNames.Any()) { return (pluginTypes, commandTypes, configurationTypes); } // we only want to load the most recent assembly in case of duplicates var assemblies = dllFileNames.Select(Assembly.LoadFrom) .Union(GetRemoteAssemblies()) .GroupBy(assembly => assembly.FullName).Select(assembly => assembly.OrderByDescending(asm => asm.GetName().Version).First()); var eligibleAssemblyTypes = assemblies .SelectMany(asm => { try { return asm.GetTypes(); } catch { return Enumerable.Empty(); } }).Where(type => FilterTypes.Any(filterType => type.GetInterface(filterType.Name, false) != null) || (type.IsClass && FilterTypes.Contains(type.BaseType))); foreach (var assemblyType in eligibleAssemblyTypes) { var isPlugin = (assemblyType.GetInterface(nameof(IPlugin), false) ?? assemblyType.GetInterface(nameof(IPluginV2), false)) != null && (!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false); if (isPlugin) { pluginTypes.Add(assemblyType); continue; } var isCommand = assemblyType.IsClass && assemblyType.BaseType == typeof(Command) && (!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false); if (isCommand) { commandTypes.Add(assemblyType); continue; } var isConfiguration = assemblyType.IsClass && assemblyType.GetInterface(nameof(IBaseConfiguration), false) != null && (!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false); if (isConfiguration) { configurationTypes.Add(assemblyType); } } _logger.LogDebug("Discovered {Count} plugin implementations", pluginTypes.Count); _logger.LogDebug("Discovered {Count} plugin commands", pluginTypes.Count); _logger.LogDebug("Discovered {Count} configuration implementations", pluginTypes.Count); return (pluginTypes, commandTypes, configurationTypes); } private IEnumerable GetRemoteAssemblies() { try { _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.LogWarning(ex, "Could not load remote assemblies"); return Enumerable.Empty(); } } private IEnumerable GetRemoteScripts() { try { _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.LogWarning(ex,"Could not load remote scripts"); return Enumerable.Empty(); } } } public enum PluginType { Binary, Script } }