using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Data; using Observatory.Framework.Interfaces; using System.IO; using Observatory.Framework; using System.Text.Json; using Observatory.Utils; using Microsoft.Security.Extensions; namespace Observatory.PluginManagement { public class PluginManager { public static PluginManager GetInstance { get { return _instance.Value; } } private static readonly Lazy _instance = new Lazy(NewPluginManager); private static PluginManager NewPluginManager() { return new PluginManager(); } public readonly List<(string error, string? detail)> errorList; public readonly List pluginPanels; public readonly List pluginTables; public readonly List<(IObservatoryWorker plugin, PluginStatus signed)> workerPlugins; public readonly List<(IObservatoryNotifier plugin, PluginStatus signed)> notifyPlugins; private readonly PluginCore core; private readonly PluginEventHandler pluginHandler; private PluginManager() { errorList = LoadPlugins(out workerPlugins, out notifyPlugins); pluginHandler = new PluginEventHandler(workerPlugins.Select(p => p.plugin), notifyPlugins.Select(p => p.plugin)); var logMonitor = LogMonitor.GetInstance; pluginPanels = new(); pluginTables = new(); logMonitor.JournalEntry += pluginHandler.OnJournalEvent; logMonitor.StatusUpdate += pluginHandler.OnStatusUpdate; logMonitor.LogMonitorStateChanged += pluginHandler.OnLogMonitorStateChanged; var ovPopup = notifyPlugins.Any(n => n.plugin.OverridePopupNotifications); var ovAudio = notifyPlugins.Any(n => n.plugin.OverrideAudioNotifications); core = new PluginCore(ovPopup, ovAudio); List errorPlugins = new(); foreach (var plugin in workerPlugins.Select(p => p.plugin)) { try { LoadSettings(plugin); plugin.Load(core); } catch (PluginException ex) { errorList.Add((FormatErrorMessage(ex), ex.StackTrace)); errorPlugins.Add(plugin); } } workerPlugins.RemoveAll(w => errorPlugins.Contains(w.plugin)); errorPlugins.Clear(); foreach (var plugin in notifyPlugins.Select(p => p.plugin)) { // Notifiers which are also workers need not be loaded again (they are the same instance). if (!plugin.GetType().IsAssignableTo(typeof(IObservatoryWorker))) { try { LoadSettings(plugin); plugin.Load(core); } catch (PluginException ex) { errorList.Add((FormatErrorMessage(ex), ex.StackTrace)); errorPlugins.Add(plugin); } catch (Exception ex) { errorList.Add(($"{plugin.ShortName}: {ex.Message}", ex.StackTrace)); errorPlugins.Add(plugin); } } } notifyPlugins.RemoveAll(n => errorPlugins.Contains(n.plugin)); core.Notification += pluginHandler.OnNotificationEvent; core.PluginMessage += pluginHandler.OnPluginMessageEvent; if (errorList.Any()) ErrorReporter.ShowErrorPopup("Plugin Load Error" + (errorList.Count > 1 ? "s" : String.Empty), errorList); } private static string FormatErrorMessage(PluginException ex) { return $"{ex.PluginName}: {ex.UserMessage}"; } private void LoadSettings(IObservatoryPlugin plugin) { string savedSettings = Properties.Core.Default.PluginSettings; Dictionary pluginSettings; if (!String.IsNullOrWhiteSpace(savedSettings)) { var settings = JsonSerializer.Deserialize>(savedSettings); if (settings != null) { pluginSettings = settings; } else { pluginSettings = new(); } } else { pluginSettings = new(); } if (pluginSettings.ContainsKey(plugin.Name)) { var settingsElement = (JsonElement)pluginSettings[plugin.Name]; var settingsObject = JsonSerializer.Deserialize(settingsElement.GetRawText(), plugin.Settings.GetType()); plugin.Settings = settingsObject; } } public static Dictionary GetSettingDisplayNames(object settings) { var settingNames = new Dictionary(); if (settings != null) { var properties = settings.GetType().GetProperties(); foreach (var property in properties) { var attrib = property.GetCustomAttribute(); if (attrib == null) { settingNames.Add(property, property.Name); } else { settingNames.Add(property, attrib.DisplayName); } } } return settingNames; } public void SaveSettings(IObservatoryPlugin plugin, object settings) { string savedSettings = Properties.Core.Default.PluginSettings; Dictionary pluginSettings; if (!String.IsNullOrWhiteSpace(savedSettings)) { pluginSettings = JsonSerializer.Deserialize>(savedSettings); } else { pluginSettings = new(); } if (pluginSettings.ContainsKey(plugin.Name)) { pluginSettings[plugin.Name] = settings; } else { pluginSettings.Add(plugin.Name, settings); } string newSettings = JsonSerializer.Serialize(pluginSettings, new JsonSerializerOptions() { ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve }); Properties.Core.Default.PluginSettings = newSettings; SettingsManager.Save(); } public void SetPluginEnabled(IObservatoryPlugin plugin, bool enabled) { pluginHandler.SetPluginEnabled(plugin, enabled); } private static List<(string, string?)> LoadPlugins(out List<(IObservatoryWorker plugin, PluginStatus signed)> observatoryWorkers, out List<(IObservatoryNotifier plugin, PluginStatus signed)> observatoryNotifiers) { observatoryWorkers = new(); observatoryNotifiers = new(); var errorList = new List<(string, string?)>(); string pluginPath = $"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins"; string? ownExe = System.Diagnostics.Process.GetCurrentProcess()?.MainModule?.FileName; FileSignatureInfo ownSig; // This will throw if ownExe is null, but that's an error condition regardless. using (var stream = File.OpenRead(ownExe ?? String.Empty)) ownSig = FileSignatureInfo.GetFromFileStream(stream); if (Directory.Exists(pluginPath)) { ExtractPlugins(pluginPath); var pluginLibraries = Directory.GetFiles($"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins", "*.dll"); foreach (var dll in pluginLibraries) { try { PluginStatus pluginStatus = PluginStatus.SigCheckDisabled; bool loadOkay = true; if (!Properties.Core.Default.AllowUnsigned) { if (ownSig.Kind == SignatureKind.Embedded) { FileSignatureInfo pluginSig; using (var stream = File.OpenRead(dll)) pluginSig = FileSignatureInfo.GetFromFileStream(stream); if (pluginSig.Kind == SignatureKind.Embedded) { if (pluginSig.SigningCertificate.Thumbprint == ownSig.SigningCertificate.Thumbprint) { pluginStatus = PluginStatus.Signed; } else { pluginStatus = PluginStatus.InvalidSignature; } } else { pluginStatus = PluginStatus.Unsigned; } } else { pluginStatus = PluginStatus.NoCert; } if (pluginStatus != PluginStatus.Signed && pluginStatus != PluginStatus.NoCert) { string pluginHash = ComputeSha512Hash(dll); if (Properties.Core.Default.UnsignedAllowed == null) Properties.Core.Default.UnsignedAllowed = new(); if (!Properties.Core.Default.UnsignedAllowed.Contains(pluginHash)) { string warning; warning = $"Unable to confirm signature of plugin library {dll}.\r\n\r\n"; warning += "Please ensure that you trust the source of this plugin before loading it.\r\n\r\n"; warning += "Do you wish to continue loading the plugin? If you load this plugin you will not be asked again for this file."; var response = MessageBox.Show(warning, "Plugin Signature Warning", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); if (response == DialogResult.OK) { Properties.Core.Default.UnsignedAllowed.Add(pluginHash); SettingsManager.Save(); } else { loadOkay = false; } } } } if (loadOkay) { string error = LoadPluginAssembly(dll, observatoryWorkers, observatoryNotifiers, pluginStatus); if (!string.IsNullOrWhiteSpace(error)) { errorList.Add((error, string.Empty)); } } } catch (Exception ex) { errorList.Add(($"ERROR: {new FileInfo(dll).Name}, {ex.Message}", ex.StackTrace ?? String.Empty)); LoadPlaceholderPlugin(dll, PluginStatus.InvalidLibrary, observatoryNotifiers); } } } return errorList; } private static string ComputeSha512Hash(string filePath) { using (var SHA512 = System.Security.Cryptography.SHA512.Create()) { using (FileStream fileStream = File.OpenRead(filePath)) return BitConverter.ToString(SHA512.ComputeHash(fileStream)).Replace("-", "").ToLowerInvariant(); } } private static void ExtractPlugins(string pluginFolder) { var files = Directory.GetFiles(pluginFolder, "*.zip") .Concat(Directory.GetFiles(pluginFolder, "*.eop")); // Elite Observatory Plugin foreach (var file in files) { try { System.IO.Compression.ZipFile.ExtractToDirectory(file, pluginFolder, true); File.Delete(file); } catch { // Just ignore files that don't extract successfully. } } } private static string LoadPluginAssembly(string dllPath, List<(IObservatoryWorker plugin, PluginStatus signed)> workers, List<(IObservatoryNotifier plugin, PluginStatus signed)> notifiers, PluginStatus pluginStatus) { string recursionGuard = string.Empty; System.Runtime.Loader.AssemblyLoadContext.Default.Resolving += (context, name) => { if ((name?.Name?.EndsWith("resources")).GetValueOrDefault(false)) { return null; } // Importing Observatory.Framework in the Explorer Lua scripts causes an attempt to reload // the assembly, just hand it back the one we already have. if ((name?.Name?.StartsWith("Observatory.Framework")).GetValueOrDefault(false) || name?.Name == "ObservatoryFramework") { return context.Assemblies.Where(a => (a.FullName?.Contains("ObservatoryFramework")).GetValueOrDefault(false)).First(); } var foundDlls = Directory.GetFileSystemEntries(new FileInfo($"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins{Path.DirectorySeparatorChar}deps").FullName, name.Name + ".dll", SearchOption.TopDirectoryOnly); if (foundDlls.Any()) { return context.LoadFromAssemblyPath(foundDlls[0]); } if (name.Name != recursionGuard && name.Name != null) { recursionGuard = name.Name; return context.LoadFromAssemblyName(name); } else { throw new Exception("Unable to load assembly " + name.Name); } }; var pluginAssembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(new FileInfo(dllPath).FullName); Type[] types; string err = string.Empty; int pluginCount = 0; try { types = pluginAssembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { types = ex.Types.OfType().ToArray(); } catch { types = Array.Empty(); } IEnumerable workerTypes = types.Where(t => t.IsAssignableTo(typeof(IObservatoryWorker))); foreach (Type worker in workerTypes) { ConstructorInfo? constructor = worker.GetConstructor(Array.Empty()); if (constructor != null) { object instance = constructor.Invoke(Array.Empty()); workers.Add(((instance as IObservatoryWorker)!, pluginStatus)); if (instance is IObservatoryNotifier) { // This is also a notifier; add to the notifier list as well, so the work and notifier are // the same instance and can share state. notifiers.Add(((instance as IObservatoryNotifier)!, pluginStatus)); } pluginCount++; } } // Filter out items which are also workers as we've already created them above. var notifyTypes = types.Where(t => t.IsAssignableTo(typeof(IObservatoryNotifier)) && !t.IsAssignableTo(typeof(IObservatoryWorker))); foreach (Type notifier in notifyTypes) { ConstructorInfo? constructor = notifier.GetConstructor(Array.Empty()); if (constructor != null) { object instance = constructor.Invoke(Array.Empty()); notifiers.Add(((instance as IObservatoryNotifier)!, pluginStatus)); pluginCount++; } } if (pluginCount == 0) { err += $"ERROR: Library '{dllPath}' contains no suitable interfaces."; LoadPlaceholderPlugin(dllPath, PluginStatus.InvalidPlugin, notifiers); } return err; } internal void Shutdown() { core.Shutdown(); } private static void LoadPlaceholderPlugin(string dllPath, PluginStatus pluginStatus, List<(IObservatoryNotifier plugin, PluginStatus signed)> notifiers) { PlaceholderPlugin placeholder = new(new FileInfo(dllPath).Name); notifiers.Add((placeholder, pluginStatus)); } /// /// Possible plugin load results and signature statuses. /// public enum PluginStatus { /// /// Plugin valid and signed with matching certificate. /// Signed, /// /// Plugin valid but not signed with any certificate. /// Unsigned, /// /// Plugin valid but not signed with valid certificate. /// InvalidSignature, /// /// Plugin invalid and cannot be loaded. Possible version mismatch. /// InvalidPlugin, /// /// Plugin not a CLR library. /// InvalidLibrary, /// /// Plugin valid but executing assembly has no certificate to match against. /// NoCert, /// /// Plugin signature checks disabled. /// SigCheckDisabled } } }