diff options
Diffstat (limited to 'VNLib.Plugins.Extensions.Loading')
16 files changed, 0 insertions, 2225 deletions
diff --git a/VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs b/VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs deleted file mode 100644 index 5baf123..0000000 --- a/VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs +++ /dev/null @@ -1,162 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: AssemblyLoader.cs -* -* AssemblyLoader.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System; -using System.Linq; -using System.Threading; -using System.Reflection; -using System.Runtime.Loader; -using System.Collections.Generic; - -using McMaster.NETCore.Plugins; - -using VNLib.Utils.Resources; - -namespace VNLib.Plugins.Extensions.Loading -{ - /// <summary> - /// <para> - /// Represents a disposable assembly loader wrapper for - /// exporting a signle type from a loaded assembly - /// </para> - /// <para> - /// If the loaded type implements <see cref="IDisposable"/> the - /// dispose method is called when the loader is disposed - /// </para> - /// </summary> - /// <typeparam name="T">The exported type to manage</typeparam> - public class AssemblyLoader<T> : OpenResourceHandle<T> - { - private readonly PluginLoader _loader; - private readonly CancellationTokenRegistration _reg; - private readonly Lazy<T> _instance; - - /// <summary> - /// The instance of the loaded type - /// </summary> - public override T Resource => _instance.Value; - - private AssemblyLoader(PluginLoader loader, in CancellationToken unloadToken) - { - _loader = loader; - //Init lazy loader - _instance = new(LoadAndGetExportedType, LazyThreadSafetyMode.PublicationOnly); - //Register dispose - _reg = unloadToken.Register(Dispose); - } - - /// <summary> - /// Loads the default assembly and gets the expected export type, - /// creates a new instance, and calls its parameterless constructor - /// </summary> - /// <returns>The desired type instance</returns> - /// <exception cref="EntryPointNotFoundException"></exception> - private T LoadAndGetExportedType() - { - //Load the assembly - Assembly asm = _loader.LoadDefaultAssembly(); - - Type resourceType = typeof(T); - - //See if the type is exported - Type exp = (from type in asm.GetExportedTypes() - where resourceType.IsAssignableFrom(type) - select type) - .FirstOrDefault() - ?? throw new EntryPointNotFoundException($"Imported assembly does not export desired type {resourceType.FullName}"); - //Create instance - return (T)Activator.CreateInstance(exp)!; - } - - /// <summary> - /// Creates a method delegate for the given method name from - /// the instance wrapped by the current loader - /// </summary> - /// <typeparam name="TDelegate"></typeparam> - /// <param name="methodName">The name of the method to recover</param> - /// <returns>The delegate method wrapper if found, null otherwise</returns> - /// <exception cref="ArgumentNullException"></exception> - /// <exception cref="AmbiguousMatchException"></exception> - public TDelegate? TryGetMethod<TDelegate>(string methodName) where TDelegate : Delegate - { - //get the type info of the actual resource - return Resource!.GetType() - .GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance) - ?.CreateDelegate<TDelegate>(Resource); - } - - ///<inheritdoc/> - protected override void Free() - { - //If the instance is disposable, call its dispose method on unload - if (_instance.IsValueCreated && _instance.Value is IDisposable) - { - (_instance.Value as IDisposable)?.Dispose(); - } - _loader.Dispose(); - _reg.Dispose(); - } - - /// <summary> - /// Creates a new assembly loader for the specified type and - /// </summary> - /// <param name="assemblyName">The name of the assmbly within the current plugin directory</param> - /// <param name="unloadToken">The plugin unload token</param> - internal static AssemblyLoader<T> Load(string assemblyName, CancellationToken unloadToken) - { - Assembly executingAsm = Assembly.GetExecutingAssembly(); - AssemblyLoadContext currentCtx = AssemblyLoadContext.GetLoadContext(executingAsm) ?? throw new InvalidOperationException("Could not get default assembly load context"); - - List<Type> shared = new () - { - typeof(T), - typeof(PluginBase), - }; - - //Share all VNLib internal libraries - shared.AddRange(currentCtx.Assemblies.Where(static s => s.FullName.Contains("VNLib", StringComparison.OrdinalIgnoreCase)).SelectMany(static s => s.GetExportedTypes())); - - PluginLoader loader = PluginLoader.CreateFromAssemblyFile(assemblyName, - currentCtx.IsCollectible, - shared.ToArray(), - conf => - { - - /* - * Load context is required to be set to the executing assembly's load context - * because it is controlled by the host, so this loader should be considered a - * a "child" collection of assemblies - */ - conf.DefaultContext = currentCtx; - - conf.PreferSharedTypes = true; - - //Share utils asm - conf.SharedAssemblies.Add(typeof(Utils.Memory.Memory).Assembly.GetName()); - }); - - return new(loader, in unloadToken); - } - } -} diff --git a/VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs b/VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs deleted file mode 100644 index 21f2fcb..0000000 --- a/VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs +++ /dev/null @@ -1,200 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: ConfigurationExtensions.cs -* -* ConfigurationExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System; -using System.Linq; -using System.Text.Json; -using System.Reflection; -using System.Collections.Generic; - -using VNLib.Utils.Extensions; - -namespace VNLib.Plugins.Extensions.Loading -{ - /// <summary> - /// Specifies a configuration variable name in the plugin's configuration - /// containing data specific to the type - /// </summary> - [AttributeUsage(AttributeTargets.Class)] - public sealed class ConfigurationNameAttribute : Attribute - { - /// <summary> - /// - /// </summary> - public string ConfigVarName { get; } - - /// <summary> - /// Initializes a new <see cref="ConfigurationNameAttribute"/> - /// </summary> - /// <param name="configVarName">The name of the configuration variable for the class</param> - public ConfigurationNameAttribute(string configVarName) - { - ConfigVarName = configVarName; - } - } - - /// <summary> - /// Contains extensions for plugin configuration specifc extensions - /// </summary> - public static class ConfigurationExtensions - { - public const string S3_CONFIG = "s3_config"; - public const string S3_SECRET_KEY = "s3_secret"; - - /// <summary> - /// Retrieves a top level configuration dictionary of elements for the specified type. - /// The type must contain a <see cref="ConfigurationNameAttribute"/> - /// </summary> - /// <typeparam name="T">The type to get the configuration of</typeparam> - /// <param name="plugin"></param> - /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns> - /// <exception cref="ObjectDisposedException"></exception> - public static IReadOnlyDictionary<string, JsonElement> GetConfigForType<T>(this PluginBase plugin) - { - Type t = typeof(T); - return plugin.GetConfigForType(t); - } - /// <summary> - /// Retrieves a top level configuration dictionary of elements with the specified property name, - /// from the plugin config first, or falls back to the host config file - /// </summary> - /// <param name="plugin"></param> - /// <param name="propName">The config property name to retrieve</param> - /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns> - /// <exception cref="KeyNotFoundException"></exception> - /// <exception cref="ObjectDisposedException"></exception> - public static IReadOnlyDictionary<string, JsonElement> GetConfig(this PluginBase plugin, string propName) - { - plugin.ThrowIfUnloaded(); - try - { - //Try to get the element from the plugin config first - if (!plugin.PluginConfig.TryGetProperty(propName, out JsonElement el)) - { - //Fallback to the host config - el = plugin.HostConfig.GetProperty(propName); - } - //Get the top level config as a dictionary - return el.EnumerateObject().ToDictionary(static k => k.Name, static k => k.Value); - } - catch(KeyNotFoundException) - { - throw new KeyNotFoundException($"Missing required top level configuration object '{propName}', in host/plugin configuration files"); - } - } - /// <summary> - /// Retrieves a top level configuration dictionary of elements with the specified property name, - /// from the plugin config first, or falls back to the host config file - /// </summary> - /// <param name="plugin"></param> - /// <param name="propName">The config property name to retrieve</param> - /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns> - /// <exception cref="ObjectDisposedException"></exception> - public static IReadOnlyDictionary<string, JsonElement>? TryGetConfig(this PluginBase plugin, string propName) - { - plugin.ThrowIfUnloaded(); - //Try to get the element from the plugin config first, or fallback to host - if (plugin.PluginConfig.TryGetProperty(propName, out JsonElement el) || plugin.HostConfig.TryGetProperty(propName, out el)) - { - //Get the top level config as a dictionary - return el.EnumerateObject().ToDictionary(static k => k.Name, static k => k.Value); - } - //No config found - return null; - } - - /// <summary> - /// Retrieves a top level configuration dictionary of elements for the specified type. - /// The type must contain a <see cref="ConfigurationNameAttribute"/> - /// </summary> - /// <param name="plugin"></param> - /// <param name="type">The type to get configuration data for</param> - /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns> - /// <exception cref="ObjectDisposedException"></exception> - public static IReadOnlyDictionary<string, JsonElement> GetConfigForType(this PluginBase plugin, Type type) - { - //Get config name attribute from plugin type - ConfigurationNameAttribute? configName = type.GetCustomAttribute<ConfigurationNameAttribute>(); - return configName?.ConfigVarName == null - ? throw new KeyNotFoundException("No configuration attribute set") - : plugin.GetConfig(configName.ConfigVarName); - } - - /// <summary> - /// Shortcut extension for <see cref="GetConfigForType{T}(PluginBase)"/> to get - /// config of current class - /// </summary> - /// <param name="obj">The object that a configuration can be retrieved for</param> - /// <param name="plugin">The plugin containing configuration variables</param> - /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns> - /// <exception cref="ObjectDisposedException"></exception> - public static IReadOnlyDictionary<string, JsonElement> GetConfig(this PluginBase plugin, object obj) - { - Type t = obj.GetType(); - return plugin.GetConfigForType(t); - } - - /// <summary> - /// Determines if the current plugin configuration contains the require properties to initialize - /// the type - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="plugin"></param> - /// <returns>True if the plugin config contains the require configuration property</returns> - public static bool HasConfigForType<T>(this PluginBase plugin) - { - Type type = typeof(T); - ConfigurationNameAttribute? configName = type.GetCustomAttribute<ConfigurationNameAttribute>(); - //See if the plugin contains a configuration varables - return configName != null && plugin.PluginConfig.TryGetProperty(configName.ConfigVarName, out _); - } - - /// <summary> - /// Attempts to load the basic S3 configuration variables required - /// for S3 client access - /// </summary> - /// <param name="plugin"></param> - /// <returns>The S3 configuration object found in the plugin/host configuration</returns> - public static S3Config? TryGetS3Config(this PluginBase plugin) - { - //Try get the config - IReadOnlyDictionary<string, JsonElement>? s3conf = plugin.TryGetConfig(S3_CONFIG); - if(s3conf == null) - { - return null; - } - - //Try get the elements - return new() - { - BaseBucket = s3conf.GetPropString("bucket"), - ClientId = s3conf.GetPropString("access_key"), - ServerAddress = s3conf.GetPropString("server_address"), - UseSsl = s3conf.TryGetValue("use_ssl", out JsonElement el) && el.GetBoolean(), - ClientSecret = plugin.TryGetSecretAsync(S3_SECRET_KEY), - Region = s3conf.GetPropString("region"), - }; - } - } -} diff --git a/VNLib.Plugins.Extensions.Loading/Events/AsyncIntervalAttribute.cs b/VNLib.Plugins.Extensions.Loading/Events/AsyncIntervalAttribute.cs deleted file mode 100644 index 85b0b6d..0000000 --- a/VNLib.Plugins.Extensions.Loading/Events/AsyncIntervalAttribute.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: AsyncIntervalAttribute.cs -* -* AsyncIntervalAttribute.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System; - -namespace VNLib.Plugins.Extensions.Loading.Events -{ - /// <summary> - /// When added to a method schedules it as a callback on a specified interval when - /// the plugin is loaded, and stops when unloaded - /// </summary> - [AttributeUsage(AttributeTargets.Method)] - public sealed class AsyncIntervalAttribute : Attribute - { - internal readonly TimeSpan Interval; - - /// <summary> - /// Intializes the <see cref="AsyncIntervalAttribute"/> with the specified timeout in milliseconds - /// </summary> - /// <param name="milliseconds">The interval in milliseconds</param> - public AsyncIntervalAttribute(int milliseconds) - { - Interval = TimeSpan.FromMilliseconds(milliseconds); - } - } -} diff --git a/VNLib.Plugins.Extensions.Loading/Events/ConfigurableAsyncIntervalAttribute.cs b/VNLib.Plugins.Extensions.Loading/Events/ConfigurableAsyncIntervalAttribute.cs deleted file mode 100644 index 12c5ec4..0000000 --- a/VNLib.Plugins.Extensions.Loading/Events/ConfigurableAsyncIntervalAttribute.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: ConfigurableAsyncIntervalAttribute.cs -* -* ConfigurableAsyncIntervalAttribute.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System; - -namespace VNLib.Plugins.Extensions.Loading.Events -{ - /// <summary> - /// When added to a method schedules it as a callback on a specified interval when - /// the plugin is loaded, and stops when unloaded - /// </summary> - [AttributeUsage(AttributeTargets.Method)] - public sealed class ConfigurableAsyncIntervalAttribute : Attribute - { - internal readonly string IntervalPropertyName; - internal readonly IntervalResultionType Resolution; - - /// <summary> - /// Initializes a <see cref="ConfigurableAsyncIntervalAttribute"/> with the specified - /// interval property name - /// </summary> - /// <param name="configPropName">The configuration property name for the event interval</param> - /// <param name="resolution">The time resoltion for the event interval</param> - public ConfigurableAsyncIntervalAttribute(string configPropName, IntervalResultionType resolution) - { - IntervalPropertyName = configPropName; - Resolution = resolution; - } - } -} diff --git a/VNLib.Plugins.Extensions.Loading/Events/EventManagment.cs b/VNLib.Plugins.Extensions.Loading/Events/EventManagment.cs deleted file mode 100644 index af55852..0000000 --- a/VNLib.Plugins.Extensions.Loading/Events/EventManagment.cs +++ /dev/null @@ -1,125 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: EventManagment.cs -* -* EventManagment.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System; -using System.Threading; -using System.Threading.Tasks; - -using VNLib.Utils.Logging; - -namespace VNLib.Plugins.Extensions.Loading.Events -{ - - /// <summary> - /// A deletage to form a method signature for shedulable interval callbacks - /// </summary> - /// <param name="log">The plugin's default log provider</param> - /// <param name="pluginExitToken">The plugin's exit token</param> - /// <returns>A task the represents the asynchronous work</returns> - public delegate Task AsyncSchedulableCallback(ILogProvider log, CancellationToken pluginExitToken); - - /// <summary> - /// Provides event schedueling extensions for plugins - /// </summary> - public static class EventManagment - { - /// <summary> - /// Schedules an asynchronous event interval for the current plugin, that is active until canceled or until the plugin unloads - /// </summary> - /// <param name="plugin"></param> - /// <param name="asyncCallback">An asyncrhonous callback method.</param> - /// <param name="interval">The event interval</param> - /// <param name="immediate">A value that indicates if the callback should be run as soon as possible</param> - /// <returns>An <see cref="EventHandle"/> that can manage the interval state</returns> - /// <exception cref="ObjectDisposedException"></exception> - /// <remarks>If exceptions are raised during callback execution, they are written to the plugin's default log provider</remarks> - public static void ScheduleInterval(this PluginBase plugin, AsyncSchedulableCallback asyncCallback, TimeSpan interval, bool immediate = false) - { - plugin.ThrowIfUnloaded(); - - plugin.Log.Verbose("Interval for {t} scheduled", interval); - - //Run interval on plugins bg scheduler - _ = plugin.DeferTask(() => RunIntervalOnPluginScheduler(plugin, asyncCallback, interval, immediate)); - } - - private static async Task RunIntervalOnPluginScheduler(PluginBase plugin, AsyncSchedulableCallback callback, TimeSpan interval, bool immediate) - { - - static async Task RunCallbackAsync(PluginBase plugin, AsyncSchedulableCallback callback) - { - try - { - //invoke interval callback - await callback(plugin.Log, plugin.UnloadToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - //unloaded - plugin.Log.Verbose("Interval callback canceled due to plugin unload or other event cancellation"); - } - catch (Exception ex) - { - plugin.Log.Error(ex, "Unhandled exception raised during timer callback"); - } - } - - //Run callback immediatly if requested - if (immediate) - { - await RunCallbackAsync(plugin, callback); - } - - //Timer loop - while (true) - { - try - { - //await delay and wait for plugin cancellation - await Task.Delay(interval, plugin.UnloadToken); - } - catch (TaskCanceledException) - { - //Unload token canceled, exit loop - break; - } - - await RunCallbackAsync(plugin, callback); - } - } - - /// <summary> - /// Registers an <see cref="IIntervalScheduleable"/> type's event handler for - /// raising timed interval events - /// </summary> - /// <param name="plugin"></param> - /// <param name="scheduleable">The instance to schedule for timeouts</param> - /// <param name="interval">The timeout interval</param> - /// <returns>An <see cref="EventHandle"/> that can manage the interval state</returns> - /// <exception cref="ObjectDisposedException"></exception> - /// <remarks>If exceptions are raised during callback execution, they are written to the plugin's default log provider</remarks> - public static void ScheduleInterval(this PluginBase plugin, IIntervalScheduleable scheduleable, TimeSpan interval) => - ScheduleInterval(plugin, scheduleable.OnIntervalAsync, interval); - } -} diff --git a/VNLib.Plugins.Extensions.Loading/Events/IIntervalScheduleable.cs b/VNLib.Plugins.Extensions.Loading/Events/IIntervalScheduleable.cs deleted file mode 100644 index 5ff40f4..0000000 --- a/VNLib.Plugins.Extensions.Loading/Events/IIntervalScheduleable.cs +++ /dev/null @@ -1,45 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: IIntervalScheduleable.cs -* -* IIntervalScheduleable.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System.Threading; -using System.Threading.Tasks; - -using VNLib.Utils.Logging; - -namespace VNLib.Plugins.Extensions.Loading.Events -{ - /// <summary> - /// Exposes a type for asynchronous event schelueling - /// </summary> - public interface IIntervalScheduleable - { - /// <summary> - /// A method that is called when the interval time has elapsed - /// </summary> - /// <param name="log">The plugin default log provider</param> - /// <param name="cancellationToken">A token that may cancel an operations if the plugin becomes unloaded</param> - /// <returns>A task that resolves when the async operation completes</returns> - Task OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken); - } -} diff --git a/VNLib.Plugins.Extensions.Loading/Events/IntervalResultionType.cs b/VNLib.Plugins.Extensions.Loading/Events/IntervalResultionType.cs deleted file mode 100644 index d82efc4..0000000 --- a/VNLib.Plugins.Extensions.Loading/Events/IntervalResultionType.cs +++ /dev/null @@ -1,49 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: IntervalResultionType.cs -* -* IntervalResultionType.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -namespace VNLib.Plugins.Extensions.Loading.Events -{ - /// <summary> - /// The configurable event interval resulution type - /// </summary> - public enum IntervalResultionType - { - /// <summary> - /// Specifies event interval resolution in milliseconds - /// </summary> - Milliseconds, - /// <summary> - /// Specifies event interval resolution in seconds - /// </summary> - Seconds, - /// <summary> - /// Specifies event interval resolution in minutes - /// </summary> - Minutes, - /// <summary> - /// Specifies event interval resolution in hours - /// </summary> - Hours - } -} diff --git a/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs b/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs deleted file mode 100644 index c23f5e2..0000000 --- a/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs +++ /dev/null @@ -1,334 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: LoadingExtensions.cs -* -* LoadingExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Runtime.CompilerServices; - -using VNLib.Utils; -using VNLib.Utils.Logging; -using VNLib.Utils.Extensions; -using VNLib.Plugins.Essentials.Accounts; - -namespace VNLib.Plugins.Extensions.Loading -{ - /// <summary> - /// Provides common loading (and unloading when required) extensions for plugins - /// </summary> - public static class LoadingExtensions - { - public const string DEBUG_CONFIG_KEY = "debug"; - public const string SECRETS_CONFIG_KEY = "secrets"; - public const string PASSWORD_HASHING_KEY = "passwords"; - - /* - * Plugin local cache used for storing singletons for a plugin instance - */ - private static readonly ConditionalWeakTable<PluginBase, PluginLocalCache> _localCache = new(); - - /// <summary> - /// Gets a previously cached service singleton for the desired plugin - /// </summary> - /// <param name="serviceType">The service instance type</param> - /// <param name="plugin">The plugin to obtain or build the singleton for</param> - /// <param name="serviceFactory">The method to produce the singleton</param> - /// <returns>The cached or newly created singleton</returns> - public static object GetOrCreateSingleton(PluginBase plugin, Type serviceType, Func<PluginBase, object> serviceFactory) - { - Lazy<object>? service; - //Get local cache - PluginLocalCache pc = _localCache.GetValue(plugin, PluginLocalCache.Create); - //Hold lock while get/set the singleton - lock (pc.SyncRoot) - { - //Check if service already exists - service = pc.GetService(serviceType); - //publish the service if it isnt loaded yet - service ??= pc.AddService(serviceType, serviceFactory); - } - //Deferred load of the service - return service.Value; - } - - /// <summary> - /// Gets a previously cached service singleton for the desired plugin - /// or creates a new singleton instance for the plugin - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="plugin">The plugin to obtain or build the singleton for</param> - /// <param name="serviceFactory">The method to produce the singleton</param> - /// <returns>The cached or newly created singleton</returns> - public static T GetOrCreateSingleton<T>(PluginBase plugin, Func<PluginBase, T> serviceFactory) - => (T)GetOrCreateSingleton(plugin, typeof(T), p => serviceFactory(p)!); - - - /// <summary> - /// Gets the plugins ambient <see cref="PasswordHashing"/> if loaded, or loads it if required. This class will - /// be unloaded when the plugin us unloaded. - /// </summary> - /// <param name="plugin"></param> - /// <returns>The ambient <see cref="PasswordHashing"/></returns> - /// <exception cref="OverflowException"></exception> - /// <exception cref="KeyNotFoundException"></exception> - /// <exception cref="ObjectDisposedException"></exception> - public static PasswordHashing GetPasswords(this PluginBase plugin) - { - plugin.ThrowIfUnloaded(); - //Get/load the passwords one time only - return GetOrCreateSingleton(plugin, LoadPasswords); - } - - private static PasswordHashing LoadPasswords(PluginBase plugin) - { - PasswordHashing Passwords; - //Get the global password system secret (pepper) - byte[] pepper = plugin.TryGetSecretAsync(PASSWORD_HASHING_KEY) - .ToBase64Bytes().Result ?? throw new KeyNotFoundException($"Missing required key '{PASSWORD_HASHING_KEY}' in secrets"); - - ERRNO cb(Span<byte> buffer) - { - //No longer valid peper if plugin is unloaded as its set to zero, so we need to protect it - plugin.ThrowIfUnloaded(); - - pepper.CopyTo(buffer); - return pepper.Length; - } - - //See hashing params are defined - IReadOnlyDictionary<string, JsonElement>? hashingArgs = plugin.TryGetConfig(PASSWORD_HASHING_KEY); - if (hashingArgs != null) - { - //Get hashing arguments - uint saltLen = hashingArgs["salt_len"].GetUInt32(); - uint hashLen = hashingArgs["hash_len"].GetUInt32(); - uint timeCost = hashingArgs["time_cost"].GetUInt32(); - uint memoryCost = hashingArgs["memory_cost"].GetUInt32(); - uint parallelism = hashingArgs["parallelism"].GetUInt32(); - //Load passwords - Passwords = new(cb, pepper.Length, (int)saltLen, timeCost, memoryCost, parallelism, hashLen); - } - else - { - //Init default password hashing - Passwords = new(cb, pepper.Length); - } - - //Register event to cleanup the password class - _ = plugin.RegisterForUnload(() => - { - //Zero the pepper - CryptographicOperations.ZeroMemory(pepper); - }); - //return - return Passwords; - } - - - /// <summary> - /// Loads an assembly into the current plugins AppDomain and will unload when disposed - /// or the plugin is unloaded from the host application. - /// </summary> - /// <typeparam name="T">The desired exported type to load from the assembly</typeparam> - /// <param name="plugin"></param> - /// <param name="assemblyName">The name of the assembly (ex: 'file.dll') to search for</param> - /// <param name="dirSearchOption">Directory/file search option</param> - /// <returns>The <see cref="AssemblyLoader{T}"/> managing the loaded assmbly in the current AppDomain</returns> - /// <exception cref="ArgumentNullException"></exception> - /// <exception cref="FileNotFoundException"></exception> - /// <exception cref="EntryPointNotFoundException"></exception> - public static AssemblyLoader<T> LoadAssembly<T>(this PluginBase plugin, string assemblyName, SearchOption dirSearchOption = SearchOption.AllDirectories) - { - plugin.ThrowIfUnloaded(); - _ = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName)); - - //get plugin directory from config - IReadOnlyDictionary<string, JsonElement> config = plugin.GetConfig("plugins"); - string? pluginsBaseDir = config["path"].GetString(); - - /* - * This should never happen since this method can only be called from a - * plugin context, which means this path was used to load the current plugin - */ - _ = pluginsBaseDir ?? throw new ArgumentNullException("path", "No plugin path is defined for the current host configuration, this is likely a bug"); - - //Get the first file that matches the search file - string? asmFile = Directory.EnumerateFiles(pluginsBaseDir, assemblyName, dirSearchOption).FirstOrDefault(); - _ = asmFile ?? throw new FileNotFoundException($"Failed to load custom assembly {assemblyName} from plugin directory"); - - //Load the assembly - return AssemblyLoader<T>.Load(asmFile, plugin.UnloadToken); - } - - - /// <summary> - /// Determintes if the current plugin config has a debug propety set - /// </summary> - /// <param name="plugin"></param> - /// <returns>True if debug mode is enabled, false otherwise</returns> - /// <exception cref="ObjectDisposedException"></exception> - public static bool IsDebug(this PluginBase plugin) - { - plugin.ThrowIfUnloaded(); - //Check for debug element - return plugin.PluginConfig.TryGetProperty(DEBUG_CONFIG_KEY, out JsonElement dbgEl) && dbgEl.GetBoolean(); - } - - /// <summary> - /// Internal exception helper to raise <see cref="ObjectDisposedException"/> if the plugin has been unlaoded - /// </summary> - /// <param name="plugin"></param> - /// <exception cref="ObjectDisposedException"></exception> - public static void ThrowIfUnloaded(this PluginBase plugin) - { - //See if the plugin was unlaoded - if (plugin.UnloadToken.IsCancellationRequested) - { - throw new ObjectDisposedException("The plugin has been unloaded"); - } - } - - /// <summary> - /// Schedules an asynchronous callback function to run and its results will be observed - /// when the operation completes, or when the plugin is unloading - /// </summary> - /// <param name="plugin"></param> - /// <param name="asyncTask">The asynchronous operation to observe</param> - /// <param name="delayMs">An optional startup delay for the operation</param> - /// <returns>A task that completes when the deferred task completes </returns> - /// <exception cref="ObjectDisposedException"></exception> - public static async Task DeferTask(this PluginBase plugin, Func<Task> asyncTask, int delayMs = 0) - { - /* - * Motivation: - * Sometimes during plugin loading, a plugin may want to asynchronously load - * data, where the results are not required to be observed during loading, but - * should not be pending after the plugin is unloaded, as the assembly may be - * unloaded and referrences collected by the GC. - * - * So we can use the plugin's unload cancellation token to observe the results - * of a pending async operation - */ - - //Test status - plugin.ThrowIfUnloaded(); - - //Optional delay - await Task.Delay(delayMs); - - //Run on ts - Task deferred = Task.Run(asyncTask); - - //Add task to deferred list - plugin.ObserveTask(deferred); - try - { - //Await the task results - await deferred.ConfigureAwait(false); - } - catch(Exception ex) - { - //Log errors - plugin.Log.Error(ex, "Error occured while observing deferred task"); - } - finally - { - //Remove task when complete - plugin.RemoveObservedTask(deferred); - } - } - - /// <summary> - /// Registers an event to occur when the plugin is unloaded on a background thread - /// and will cause the Plugin.Unload() method to block until the event completes - /// </summary> - /// <param name="pbase"></param> - /// <param name="callback">The method to call when the plugin is unloaded</param> - /// <returns>A task that represents the registered work</returns> - /// <exception cref="ArgumentNullException"></exception> - /// <exception cref="ObjectDisposedException"></exception> - public static Task RegisterForUnload(this PluginBase pbase, Action callback) - { - //Test status - pbase.ThrowIfUnloaded(); - _ = callback ?? throw new ArgumentNullException(nameof(callback)); - - //Wait method - static async Task WaitForUnload(PluginBase pb, Action callback) - { - //Wait for unload as a task on the threadpool to avoid deadlocks - await pb.UnloadToken.WaitHandle.WaitAsync() - .ConfigureAwait(false); - - callback(); - } - - //Registaer the task to cause the plugin to wait - return pbase.DeferTask(() => WaitForUnload(pbase, callback)); - } - - - private sealed class PluginLocalCache - { - private readonly PluginBase _plugin; - - private readonly Dictionary<Type, Lazy<object>> _store; - - public object SyncRoot { get; } = new(); - - private PluginLocalCache(PluginBase plugin) - { - _plugin = plugin; - _store = new(); - //Register cleanup on unload - _ = _plugin.RegisterForUnload(() => _store.Clear()); - } - - public static PluginLocalCache Create(PluginBase plugin) => new(plugin); - - - public Lazy<object>? GetService(Type serviceType) - { - Lazy<object>? t = _store.Where(t => t.Key.IsAssignableTo(serviceType)) - .Select(static tk => tk.Value) - .FirstOrDefault(); - return t; - } - - public Lazy<object> AddService(Type serviceType, Func<PluginBase, object> factory) - { - //Get lazy loader to invoke factory outside of cache lock - Lazy<object> lazyFactory = new(() => factory(_plugin), true); - //Store lazy factory - _store.Add(serviceType, lazyFactory); - //Pass the lazy factory back - return lazyFactory; - } - } - } -} diff --git a/VNLib.Plugins.Extensions.Loading/PrivateKey.cs b/VNLib.Plugins.Extensions.Loading/PrivateKey.cs deleted file mode 100644 index 336f6a4..0000000 --- a/VNLib.Plugins.Extensions.Loading/PrivateKey.cs +++ /dev/null @@ -1,102 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: PrivateKey.cs -* -* PrivateKey.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System; -using System.Text; -using System.Security.Cryptography; - -using VNLib.Utils; -using VNLib.Utils.Memory; -using VNLib.Utils.Extensions; - -namespace VNLib.Plugins.Extensions.Loading -{ - /// <summary> - /// A container for a PKSC#8 encoed private key - /// </summary> - public sealed class PrivateKey : VnDisposeable - { - private readonly byte[] _utf8RawData; - - /// <summary> - /// Decodes the PKCS#8 encoded private key from a secret, as an EC private key - /// and recovers the ECDsa algorithm from the key - /// </summary> - /// <returns>The <see cref="ECDsa"/> algoritm from the private key</returns> - /// <exception cref="ArgumentNullException"></exception> - /// <exception cref="CryptographicException"></exception> - public ECDsa GetECDsa() - { - //Alloc buffer - using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(_utf8RawData.Length); - //Get base64 bytes from utf8 - ERRNO count = VnEncoding.Base64UrlDecode(_utf8RawData, buffer.Span); - //Parse the private key - ECDsa alg = ECDsa.Create(); - alg.ImportPkcs8PrivateKey(buffer.Span[..(int)count], out _); - //Wipe the buffer - Memory.InitializeBlock(buffer.Span); - return alg; - } - - /// <summary> - /// Decodes the PKCS#8 encoded private key from a secret, as an RSA private key - /// </summary> - /// <returns>The <see cref="RSA"/> algorithm from the private key</returns> - /// <exception cref="ArgumentNullException"></exception> - /// <exception cref="CryptographicException"></exception> - public RSA GetRSA() - { - //Alloc buffer - using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(_utf8RawData.Length); - //Get base64 bytes from utf8 - ERRNO count = VnEncoding.Base64UrlDecode(_utf8RawData, buffer.Span); - //Parse the private key - RSA alg = RSA.Create(); - alg.ImportPkcs8PrivateKey(buffer.Span[..(int)count], out _); - //Wipe the buffer - Memory.InitializeBlock(buffer.Span); - return alg; - } - - internal PrivateKey(SecretResult secret) - { - //Alloc and get utf8 - byte[] buffer = new byte[secret.Result.Length]; - int count = Encoding.UTF8.GetBytes(secret.Result, buffer); - //Verify length - if(count != buffer.Length) - { - throw new FormatException("UTF8 deocde failed"); - } - //Store - _utf8RawData = buffer; - } - - protected override void Free() - { - Memory.InitializeBlock(_utf8RawData.AsSpan()); - } - } -} diff --git a/VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs b/VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs deleted file mode 100644 index 9242522..0000000 --- a/VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs +++ /dev/null @@ -1,161 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: RoutingExtensions.cs -* -* RoutingExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System; -using System.Linq; -using System.Text.Json; -using System.Reflection; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -using VNLib.Plugins.Extensions.Loading.Events; - -namespace VNLib.Plugins.Extensions.Loading.Routing -{ - /// <summary> - /// Provides advanced QOL features to plugin loading - /// </summary> - public static class RoutingExtensions - { - private static readonly ConditionalWeakTable<IEndpoint, PluginBase?> _pluginRefs = new(); - - /// <summary> - /// Constructs and routes the specific endpoint type for the current plugin - /// </summary> - /// <typeparam name="T">The <see cref="IEndpoint"/> type</typeparam> - /// <param name="plugin"></param> - /// <param name="pluginConfigPathName">The path to the plugin sepcific configuration property</param> - /// <exception cref="TargetInvocationException"></exception> - public static T Route<T>(this PluginBase plugin, string? pluginConfigPathName) where T : IEndpoint - { - Type endpointType = typeof(T); - //If the config attribute is not set, then ignore the config variables - if (string.IsNullOrWhiteSpace(pluginConfigPathName)) - { - ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase) }); - _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}"); - //Create the new endpoint and pass the plugin instance - T endpoint = (T)constructor.Invoke(new object[] { plugin }); - //Register event handlers for the endpoint - ScheduleIntervals(plugin, endpoint, endpointType, null); - //Route the endpoint - plugin.Route(endpoint); - - //Store ref to plugin for endpoint - _pluginRefs.Add(endpoint, plugin); - - return endpoint; - } - else - { - ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IReadOnlyDictionary<string, JsonElement>) }); - //Make sure the constructor exists - _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}"); - //Get config variables for the endpoint - IReadOnlyDictionary<string, JsonElement> conf = plugin.GetConfig(pluginConfigPathName); - //Create the new endpoint and pass the plugin instance along with the configuration object - T endpoint = (T)constructor.Invoke(new object[] { plugin, conf }); - //Register event handlers for the endpoint - ScheduleIntervals(plugin, endpoint, endpointType, conf); - //Route the endpoint - plugin.Route(endpoint); - - //Store ref to plugin for endpoint - _pluginRefs.Add(endpoint, plugin); - - return endpoint; - } - } - - /// <summary> - /// Constructs and routes the specific endpoint type for the current plugin - /// </summary> - /// <typeparam name="T">The <see cref="IEndpoint"/> type</typeparam> - /// <param name="plugin"></param> - /// <exception cref="TargetInvocationException"></exception> - public static T Route<T>(this PluginBase plugin) where T : IEndpoint - { - Type endpointType = typeof(T); - //Get config name attribute - ConfigurationNameAttribute? configAttr = endpointType.GetCustomAttribute<ConfigurationNameAttribute>(); - //Route using attribute - return plugin.Route<T>(configAttr?.ConfigVarName); - } - - /// <summary> - /// Gets the plugin that loaded the current endpoint - /// </summary> - /// <param name="ep"></param> - /// <returns>The plugin that loaded the current endpoint</returns> - /// <exception cref="InvalidOperationException"></exception> - public static PluginBase GetPlugin(this IEndpoint ep) - { - _ = _pluginRefs.TryGetValue(ep, out PluginBase? pBase); - return pBase ?? throw new InvalidOperationException("Endpoint was not dynamically routed"); - } - - private static void ScheduleIntervals<T>(PluginBase plugin, T endpointInstance, Type epType, IReadOnlyDictionary<string, JsonElement>? endpointLocalConfig) where T : IEndpoint - { - //Get all methods that have the configureable async interval attribute specified - IEnumerable<Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback>> confIntervals = epType.GetMethods() - .Where(m => m.GetCustomAttribute<ConfigurableAsyncIntervalAttribute>() != null) - .Select(m => new Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback> - (m.GetCustomAttribute<ConfigurableAsyncIntervalAttribute>()!, m.CreateDelegate<AsyncSchedulableCallback>(endpointInstance))); - - //If the endpoint has a local config, then use it to find the interval - if (endpointLocalConfig != null) - { - - //Schedule event handlers on the current plugin - foreach (Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback> interval in confIntervals) - { - int value = endpointLocalConfig[interval.Item1.IntervalPropertyName].GetInt32(); - //Get the timeout from its resolution variable - TimeSpan timeout = interval.Item1.Resolution switch - { - IntervalResultionType.Seconds => TimeSpan.FromSeconds(value), - IntervalResultionType.Minutes => TimeSpan.FromMinutes(value), - IntervalResultionType.Hours => TimeSpan.FromHours(value), - _ => TimeSpan.FromMilliseconds(value), - }; - //Schedule - plugin.ScheduleInterval(interval.Item2, timeout); - } - } - - //Get all methods that have the async interval attribute specified - IEnumerable<Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback>> intervals = epType.GetMethods() - .Where(m => m.GetCustomAttribute<AsyncIntervalAttribute>() != null) - .Select(m => new Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback>( - m.GetCustomAttribute<AsyncIntervalAttribute>()!, m.CreateDelegate<AsyncSchedulableCallback>(endpointInstance)) - ); - - //Schedule event handlers on the current plugin - foreach (Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback> interval in intervals) - { - plugin.ScheduleInterval(interval.Item2, interval.Item1.Interval); - } - } - } -} diff --git a/VNLib.Plugins.Extensions.Loading/S3Config.cs b/VNLib.Plugins.Extensions.Loading/S3Config.cs deleted file mode 100644 index 6d4ae4d..0000000 --- a/VNLib.Plugins.Extensions.Loading/S3Config.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: S3Config.cs -* -* S3Config.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System.Threading.Tasks; - -namespace VNLib.Plugins.Extensions.Loading -{ - public sealed class S3Config - { - public string? ServerAddress { get; init; } - public string? ClientId { get; init; } - public Task<SecretResult?> ClientSecret { get; init; } - public string? BaseBucket { get; init; } - public bool? UseSsl { get; init; } - public string? Region { get; init; } - - public S3Config() - { - ClientSecret = Task.FromResult<SecretResult?>(null); - } - } -} diff --git a/VNLib.Plugins.Extensions.Loading/SecretResult.cs b/VNLib.Plugins.Extensions.Loading/SecretResult.cs deleted file mode 100644 index 15323f3..0000000 --- a/VNLib.Plugins.Extensions.Loading/SecretResult.cs +++ /dev/null @@ -1,61 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: SecretResult.cs -* -* SecretResult.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System; - -using VNLib.Utils; -using VNLib.Utils.Extensions; -using VNLib.Utils.Memory; - -namespace VNLib.Plugins.Extensions.Loading -{ - /// <summary> - /// The result of a secret fetch operation - /// </summary> - public sealed class SecretResult : VnDisposeable - { - private readonly char[] _secretChars; - - /// <summary> - /// The protected raw result value - /// </summary> - public ReadOnlySpan<char> Result => _secretChars; - - - internal SecretResult(ReadOnlySpan<char> value) => _secretChars = value.ToArray(); - - ///<inheritdoc/> - protected override void Free() - { - Memory.InitializeBlock(_secretChars.AsSpan()); - } - - internal static SecretResult ToSecret(string? result) - { - SecretResult res = new(result.AsSpan()); - Memory.UnsafeZeroMemory<char>(result); - return res; - } - } -} diff --git a/VNLib.Plugins.Extensions.Loading/UserLoading.cs b/VNLib.Plugins.Extensions.Loading/UserLoading.cs deleted file mode 100644 index da090ec..0000000 --- a/VNLib.Plugins.Extensions.Loading/UserLoading.cs +++ /dev/null @@ -1,93 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: UserLoading.cs -* -* UserLoading.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System; -using System.Collections.Generic; - -using VNLib.Utils.Logging; -using VNLib.Utils.Extensions; -using VNLib.Plugins.Essentials.Users; - -namespace VNLib.Plugins.Extensions.Loading.Users -{ - /// <summary> - /// Contains extension methods for plugins to load the "users" system - /// </summary> - public static class UserLoading - { - public const string USER_CUSTOM_ASSEMBLY = "user_custom_asm"; - public const string DEFAULT_USER_ASM = "VNLib.Plugins.Essentials.Users.dll"; - public const string ONLOAD_METHOD_NAME = "OnPluginLoading"; - - - /// <summary> - /// Gets or loads the plugin's ambient <see cref="IUserManager"/>, with the specified user-table name, - /// or the default table name - /// </summary> - /// <param name="plugin"></param> - /// <returns>The ambient <see cref="IUserManager"/> for the current plugin</returns> - /// <exception cref="KeyNotFoundException"></exception> - /// <exception cref="ObjectDisposedException"></exception> - public static IUserManager GetUserManager(this PluginBase plugin) - { - plugin.ThrowIfUnloaded(); - //Get stored or load - return LoadingExtensions.GetOrCreateSingleton(plugin, LoadUsers); - } - - private static IUserManager LoadUsers(PluginBase pbase) - { - //Try to load a custom user assembly for exporting IUserManager - string? customAsm = pbase.PluginConfig.GetPropString(USER_CUSTOM_ASSEMBLY); - //See if host config defined the path - customAsm ??= pbase.HostConfig.GetPropString(USER_CUSTOM_ASSEMBLY); - //Finally default - customAsm ??= DEFAULT_USER_ASM; - - //Try to load a custom assembly - AssemblyLoader<IUserManager> loader = pbase.LoadAssembly<IUserManager>(customAsm); - try - { - //Try to get the onload method - Action<object>? onLoadMethod = loader.TryGetMethod<Action<object>>(ONLOAD_METHOD_NAME); - - //Call the onplugin load method - onLoadMethod?.Invoke(pbase); - - if (pbase.IsDebug()) - { - pbase.Log.Verbose("Loading user manager from assembly {name}", loader.Resource.GetType().AssemblyQualifiedName); - } - - //Return the loaded instance (may raise exception) - return loader.Resource; - } - catch - { - loader.Dispose(); - throw; - } - } - } -}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj b/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj deleted file mode 100644 index 15bb15e..0000000 --- a/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj +++ /dev/null @@ -1,39 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> - <Authors>Vaughn Nugent</Authors> - <Version>1.0.1.1</Version> - - <GenerateDocumentationFile>True</GenerateDocumentationFile> - <Nullable>enable</Nullable> - <SignAssembly>True</SignAssembly> - <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile> - </PropertyGroup> - - <PropertyGroup> - <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> - <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> - <AnalysisLevel>latest-all</AnalysisLevel> - </PropertyGroup> - - <ItemGroup> - <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - </PackageReference> - <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - </PackageReference> - <PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" /> - <PackageReference Include="VaultSharp" Version="1.7.1" /> - </ItemGroup> - - <ItemGroup> - <ProjectReference Include="..\..\..\VNLib\Essentials\src\VNLib.Plugins.Essentials.csproj" /> - <ProjectReference Include="..\..\PluginBase\VNLib.Plugins.PluginBase.csproj" /> - </ItemGroup> - -</Project> diff --git a/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.xml b/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.xml deleted file mode 100644 index 963f506..0000000 --- a/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.xml +++ /dev/null @@ -1,260 +0,0 @@ -<?xml version="1.0"?> -<!-- -Copyright (c) 2022 Vaughn Nugent ---> -<doc> - <assembly> - <name>VNLib.Plugins.Extensions.Loading</name> - </assembly> - <members> - <member name="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute"> - <summary> - Specifies a configuration variable name in the plugin's configuration - containing data specific to the type - </summary> - </member> - <member name="F:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute.ConfigVarName"> - <summary> - - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute.#ctor(System.String)"> - <summary> - Initializes a new <see cref="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute"/> - </summary> - <param name="configVarName">The name of the configuration variable for the class</param> - </member> - <member name="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions"> - <summary> - Contains extensions for plugin configuration specifc extensions - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfigForType``1(VNLib.Plugins.PluginBase)"> - <summary> - Retrieves a top level configuration dictionary of elements for the specified type. - The type must contain a <see cref="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute"/> - </summary> - <typeparam name="T">The type to get the configuration of</typeparam> - <param name="plugin"></param> - <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns> - <exception cref="T:System.ObjectDisposedException"></exception> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfig(VNLib.Plugins.PluginBase,System.String)"> - <summary> - Retrieves a top level configuration dictionary of elements with the specified property name, - from the plugin config first, or falls back to the host config file - </summary> - <param name="plugin"></param> - <param name="propName">The config property name to retrieve</param> - <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns> - <exception cref="T:System.ObjectDisposedException"></exception> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.TryGetConfig(VNLib.Plugins.PluginBase,System.String)"> - <summary> - Retrieves a top level configuration dictionary of elements with the specified property name, - from the plugin config first, or falls back to the host config file - </summary> - <param name="plugin"></param> - <param name="propName">The config property name to retrieve</param> - <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns> - <exception cref="T:System.ObjectDisposedException"></exception> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfigForType(VNLib.Plugins.PluginBase,System.Type)"> - <summary> - Retrieves a top level configuration dictionary of elements for the specified type. - The type must contain a <see cref="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute"/> - </summary> - <param name="plugin"></param> - <param name="type">The type to get configuration data for</param> - <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns> - <exception cref="T:System.ObjectDisposedException"></exception> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfig(VNLib.Plugins.PluginBase,System.Object)"> - <summary> - Shortcut extension for <see cref="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfigForType``1(VNLib.Plugins.PluginBase)"/> to get - config of current class - </summary> - <param name="obj">The object that a configuration can be retrieved for</param> - <param name="plugin">The plugin containing configuration variables</param> - <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns> - <exception cref="T:System.ObjectDisposedException"></exception> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.HasConfigForType``1(VNLib.Plugins.PluginBase)"> - <summary> - Determines if the current plugin configuration contains the require properties to initialize - the type - </summary> - <typeparam name="T"></typeparam> - <param name="plugin"></param> - <returns>True if the plugin config contains the require configuration property</returns> - </member> - <member name="T:VNLib.Plugins.Extensions.Loading.Events.AsyncIntervalAttribute"> - <summary> - When added to a method schedules it as a callback on a specified interval when - the plugin is loaded, and stops when unloaded - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Events.AsyncIntervalAttribute.#ctor(System.Int32)"> - <summary> - Intializes the <see cref="T:VNLib.Plugins.Extensions.Loading.Events.AsyncIntervalAttribute"/> with the specified timeout in milliseconds - </summary> - <param name="milliseconds">The interval in milliseconds</param> - </member> - <member name="T:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType"> - <summary> - The configurable event interval resulution type - </summary> - </member> - <member name="F:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType.Milliseconds"> - <summary> - Specifies event interval resolution in milliseconds - </summary> - </member> - <member name="F:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType.Seconds"> - <summary> - Specifies event interval resolution in seconds - </summary> - </member> - <member name="F:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType.Minutes"> - <summary> - Specifies event interval resolution in minutes - </summary> - </member> - <member name="F:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType.Hours"> - <summary> - Specifies event interval resolution in hours - </summary> - </member> - <member name="T:VNLib.Plugins.Extensions.Loading.Events.ConfigurableAsyncIntervalAttribute"> - <summary> - When added to a method schedules it as a callback on a specified interval when - the plugin is loaded, and stops when unloaded - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Events.ConfigurableAsyncIntervalAttribute.#ctor(System.String,VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType)"> - <summary> - Initializes a <see cref="T:VNLib.Plugins.Extensions.Loading.Events.ConfigurableAsyncIntervalAttribute"/> with the specified - interval property name - </summary> - <param name="configPropName">The configuration property name for the event interval</param> - <param name="resolution">The time resoltion for the event interval</param> - </member> - <member name="T:VNLib.Plugins.Extensions.Loading.Events.EventHandle"> - <summary> - Represents a handle to a scheduled event interval that is managed by the plugin but may be cancled by disposing the instance - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Events.EventHandle.Pause"> - <summary> - Pauses the event timer until the <see cref="T:VNLib.Utils.OpenHandle"/> is released or disposed - then resumes to the inital interval period - </summary> - <returns>A <see cref="T:VNLib.Utils.OpenHandle"/> that restores the timer to its initial state when disposed</returns> - <exception cref="T:System.ObjectDisposedException"></exception> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Events.EventHandle.Free"> - <inheritdoc/> - </member> - <member name="T:VNLib.Plugins.Extensions.Loading.Events.EventManagment"> - <summary> - Provides event schedueling extensions for plugins - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Events.EventManagment.ScheduleInterval``1(VNLib.Plugins.PluginBase,System.Func{``0,System.Threading.Tasks.Task},``0,System.TimeSpan)"> - <summary> - Schedules an asynchronous event interval for the current plugin, that is active until canceled or until the plugin unloads - </summary> - <typeparam name="TState">Stateful event argument</typeparam> - <param name="plugin"></param> - <param name="asyncCallback">An asyncrhonous callback method.</param> - <param name="state"></param> - <param name="interval">The event interval</param> - <returns>An <see cref="T:VNLib.Plugins.Extensions.Loading.Events.EventHandle"/> that can manage the interval state</returns> - <exception cref="T:System.ObjectDisposedException"></exception> - <remarks>If exceptions are raised during callback execution, they are written to the plugin's default log provider</remarks> - </member> - <member name="T:VNLib.Plugins.Extensions.Loading.LoadingExtensions"> - <summary> - Provides common loading (and unloading when required) extensions for plugins - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.LoadingExtensions.GetPasswords(VNLib.Plugins.PluginBase)"> - <summary> - Gets the plugins ambient <see cref="T:VNLib.Plugins.Essentials.Accounts.PasswordHashing"/> if loaded, or loads it if required. This class will - be unloaded when the plugin us unloaded. - </summary> - <param name="plugin"></param> - <returns>The ambient <see cref="T:VNLib.Plugins.Essentials.Accounts.PasswordHashing"/></returns> - <exception cref="T:System.OverflowException"></exception> - <exception cref="T:System.Collections.Generic.KeyNotFoundException"></exception> - <exception cref="T:System.ObjectDisposedException"></exception> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.LoadingExtensions.GetUserManager(VNLib.Plugins.PluginBase)"> - <summary> - Gets or loads the plugin's ambient <see cref="T:VNLib.Plugins.Essentials.Users.UserManager"/>, with the specified user-table name, - or the default table name - </summary> - <param name="plugin"></param> - <returns>The ambient <see cref="T:VNLib.Plugins.Essentials.Users.UserManager"/> for the current plugin</returns> - <exception cref="T:System.Collections.Generic.KeyNotFoundException"></exception> - <exception cref="T:System.ObjectDisposedException"></exception> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.LoadingExtensions.IsDebug(VNLib.Plugins.PluginBase)"> - <summary> - Determintes if the current plugin config has a debug propety set - </summary> - <param name="plugin"></param> - <returns>True if debug mode is enabled, false otherwise</returns> - <exception cref="T:System.ObjectDisposedException"></exception> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.LoadingExtensions.ThrowIfUnloaded(VNLib.Plugins.PluginBase)"> - <summary> - Internal exception helper to raise <see cref="T:System.ObjectDisposedException"/> if the plugin has been unlaoded - </summary> - <param name="plugin"></param> - <exception cref="T:System.ObjectDisposedException"></exception> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Routing.RoutingExtensions.Route``1(VNLib.Plugins.PluginBase,System.String)"> - <summary> - Constructs and routes the specific endpoint type for the current plugin - </summary> - <typeparam name="T">The <see cref="T:VNLib.Plugins.IEndpoint"/> type</typeparam> - <param name="plugin"></param> - <param name="pluginConfigPathName">The path to the plugin sepcific configuration property</param> - <exception cref="T:System.Reflection.TargetInvocationException"></exception> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Routing.RoutingExtensions.Route``1(VNLib.Plugins.PluginBase)"> - <summary> - Constructs and routes the specific endpoint type for the current plugin - </summary> - <typeparam name="T">The <see cref="T:VNLib.Plugins.IEndpoint"/> type</typeparam> - <param name="plugin"></param> - <exception cref="T:System.Reflection.TargetInvocationException"></exception> - </member> - <member name="T:VNLib.Plugins.Extensions.Loading.Sql.SqlDbConnectionLoader"> - <summary> - Provides common basic SQL loading extensions for plugins - </summary> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Sql.SqlDbConnectionLoader.GetConnectionFactory(VNLib.Plugins.PluginBase)"> - <summary> - Gets (or loads) the ambient sql connection factory for the current plugin - </summary> - <param name="plugin"></param> - <returns>The ambient <see cref="T:System.Data.Common.DbConnection"/> factory</returns> - <exception cref="T:System.Collections.Generic.KeyNotFoundException"></exception> - <exception cref="T:System.ObjectDisposedException"></exception> - </member> - <member name="M:VNLib.Plugins.Extensions.Loading.Sql.SqlDbConnectionLoader.GetContextOptions(VNLib.Plugins.PluginBase)"> - <summary> - Gets (or loads) the ambient <see cref="T:Microsoft.EntityFrameworkCore.DbContextOptions"/> configured from - the ambient sql factory - </summary> - <param name="plugin"></param> - <returns>The ambient <see cref="T:Microsoft.EntityFrameworkCore.DbContextOptions"/> for the current plugin</returns> - <exception cref="T:System.Collections.Generic.KeyNotFoundException"></exception> - <exception cref="T:System.ObjectDisposedException"></exception> - <remarks>If plugin is in debug mode, writes log data to the default log</remarks> - </member> - </members> -</doc> diff --git a/VNLib.Plugins.Extensions.Loading/VaultSecrets.cs b/VNLib.Plugins.Extensions.Loading/VaultSecrets.cs deleted file mode 100644 index cd67903..0000000 --- a/VNLib.Plugins.Extensions.Loading/VaultSecrets.cs +++ /dev/null @@ -1,453 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: VaultSecrets.cs -* -* VaultSecrets.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. -*/ - -using System; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; - -using VaultSharp; -using VaultSharp.V1.Commons; -using VaultSharp.V1.AuthMethods; -using VaultSharp.V1.AuthMethods.Token; -using VaultSharp.V1.AuthMethods.AppRole; -using VaultSharp.V1.SecretsEngines.PKI; - -using VNLib.Utils; -using VNLib.Utils.Memory; -using VNLib.Utils.Logging; -using VNLib.Utils.Extensions; -using VNLib.Hashing.IdentityUtility; - -namespace VNLib.Plugins.Extensions.Loading -{ - - /// <summary> - /// Adds loading extensions for secure/centralized configuration secrets - /// </summary> - public static class PluginSecretLoading - { - public const string VAULT_OBJECT_NAME = "hashicorp_vault"; - public const string SECRETS_CONFIG_KEY = "secrets"; - public const string VAULT_TOKEN_KEY = "token"; - public const string VAULT_ROLE_KEY = "role"; - public const string VAULT_SECRET_KEY = "secret"; - - public const string VAULT_URL_KEY = "url"; - - public const string VAULT_URL_SCHEME = "vault://"; - - - /// <summary> - /// <para> - /// Gets a secret from the "secrets" element. - /// </para> - /// <para> - /// Secrets elements are merged from the host config and plugin local config 'secrets' element. - /// before searching. The plugin config takes precedence over the host config. - /// </para> - /// </summary> - /// <param name="plugin"></param> - /// <param name="secretName">The name of the secret propery to get</param> - /// <returns>The element from the configuration file with the given name, or null if the configuration or property does not exist</returns> - /// <exception cref="KeyNotFoundException"></exception> - /// <exception cref="ObjectDisposedException"></exception> - public static Task<SecretResult?> TryGetSecretAsync(this PluginBase plugin, string secretName) - { - //Get the secret from the config file raw - string? rawSecret = TryGetSecretInternal(plugin, secretName); - if (rawSecret == null) - { - return Task.FromResult<SecretResult?>(null); - } - - //Secret is a vault path, or return the raw value - if (!rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) - { - return Task.FromResult<SecretResult?>(new(rawSecret.AsSpan())); - } - return GetSecretFromVaultAsync(plugin, rawSecret); - } - - /// <summary> - /// Gets a secret at the given vault url (in the form of "vault://[mount-name]/[secret-path]?secret=[secret_name]") - /// </summary> - /// <param name="plugin"></param> - /// <param name="vaultPath">The raw vault url to lookup</param> - /// <returns>The string of the object at the specified vault path</returns> - /// <exception cref="UriFormatException"></exception> - /// <exception cref="KeyNotFoundException"></exception> - /// <exception cref="ObjectDisposedException"></exception> - public static Task<SecretResult?> GetSecretFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath) - { - //print the path for debug - if (plugin.IsDebug()) - { - plugin.Log.Debug("Retrieving secret {s} from vault", vaultPath.ToString()); - } - - //Slice off path - ReadOnlySpan<char> paq = vaultPath.SliceAfterParam(VAULT_URL_SCHEME); - ReadOnlySpan<char> path = paq.SliceBeforeParam('?'); - ReadOnlySpan<char> query = paq.SliceAfterParam('?'); - - if (paq.IsEmpty) - { - throw new UriFormatException("Vault secret location not valid/empty "); - } - //Get the secret - string secretTableKey = query.SliceAfterParam("secret=").SliceBeforeParam('&').ToString(); - string vaultType = query.SliceBeforeParam("vault_type=").SliceBeforeParam('&').ToString(); - - //get mount and path - int lastSep = path.IndexOf('/'); - string mount = path[..lastSep].ToString(); - string secret = path[(lastSep + 1)..].ToString(); - - async Task<SecretResult?> execute() - { - //Try load client - IVaultClient? client = plugin.GetVault(); - - _ = client ?? throw new KeyNotFoundException("Vault client not found"); - //run read async - Secret<SecretData> result = await client.V1.Secrets.KeyValue.V2.ReadSecretAsync(path:secret, mountPoint:mount); - //Read the secret - return SecretResult.ToSecret(result.Data.Data[secretTableKey].ToString()); - } - - return Task.Run(execute); - } - - /// <summary> - /// <para> - /// Gets a Certicate from the "secrets" element. - /// </para> - /// <para> - /// Secrets elements are merged from the host config and plugin local config 'secrets' element. - /// before searching. The plugin config takes precedence over the host config. - /// </para> - /// </summary> - /// <param name="plugin"></param> - /// <param name="secretName">The name of the secret propery to get</param> - /// <returns>The element from the configuration file with the given name, or null if the configuration or property does not exist</returns> - /// <exception cref="KeyNotFoundException"></exception> - /// <exception cref="ObjectDisposedException"></exception> - public static Task<X509Certificate?> TryGetCertificateAsync(this PluginBase plugin, string secretName) - { - //Get the secret from the config file raw - string? rawSecret = TryGetSecretInternal(plugin, secretName); - if (rawSecret == null) - { - return Task.FromResult<X509Certificate?>(null); - } - - //Secret is a vault path, or return the raw value - if (!rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) - { - return Task.FromResult<X509Certificate?>(new (rawSecret)); - } - return GetCertFromVaultAsync(plugin, rawSecret); - } - - public static Task<X509Certificate?> GetCertFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath, CertificateCredentialsRequestOptions? options = null) - { - //print the path for debug - if (plugin.IsDebug()) - { - plugin.Log.Debug("Retrieving certificate {s} from vault", vaultPath.ToString()); - } - - //Slice off path - ReadOnlySpan<char> paq = vaultPath.SliceAfterParam(VAULT_URL_SCHEME); - ReadOnlySpan<char> path = paq.SliceBeforeParam('?'); - ReadOnlySpan<char> query = paq.SliceAfterParam('?'); - - if (paq.IsEmpty) - { - throw new UriFormatException("Vault secret location not valid/empty "); - } - - //Get the secret - string role = query.SliceAfterParam("role=").SliceBeforeParam('&').ToString(); - string vaultType = query.SliceBeforeParam("vault_type=").SliceBeforeParam('&').ToString(); - string commonName = query.SliceBeforeParam("cn=").SliceBeforeParam('&').ToString(); - - //get mount and path - int lastSep = path.IndexOf('/'); - string mount = path[..lastSep].ToString(); - string secret = path[(lastSep + 1)..].ToString(); - - async Task<X509Certificate?> execute() - { - //Try load client - IVaultClient? client = plugin.GetVault(); - - _ = client ?? throw new KeyNotFoundException("Vault client not found"); - - options ??= new() - { - CertificateFormat = CertificateFormat.pem, - PrivateKeyFormat = PrivateKeyFormat.pkcs8, - CommonName = commonName, - }; - - //run read async - Secret<CertificateCredentials> result = await client.V1.Secrets.PKI.GetCredentialsAsync(pkiRoleName:secret, certificateCredentialRequestOptions:options, pkiBackendMountPoint:mount); - //Read the secret - byte[] pemCertData = Encoding.UTF8.GetBytes(result.Data.CertificateContent); - - return new (pemCertData); - } - - return Task.Run(execute); - } - - /// <summary> - /// Gets the ambient vault client for the current plugin - /// if the configuration is loaded, null otherwise - /// </summary> - /// <param name="plugin"></param> - /// <returns>The ambient <see cref="IVaultClient"/> if loaded, null otherwise</returns> - /// <exception cref="KeyNotFoundException"></exception> - /// <exception cref="ObjectDisposedException"></exception> - public static IVaultClient? GetVault(this PluginBase plugin) => LoadingExtensions.GetOrCreateSingleton(plugin, TryGetVaultLoader); - - private static string? TryGetSecretInternal(PluginBase plugin, string secretName) - { - bool local = plugin.PluginConfig.TryGetProperty(SECRETS_CONFIG_KEY, out JsonElement localEl); - bool host = plugin.HostConfig.TryGetProperty(SECRETS_CONFIG_KEY, out JsonElement hostEl); - - //total config - IReadOnlyDictionary<string, JsonElement>? conf; - - if (local && host) - { - //Load both config objects to dict - Dictionary<string, JsonElement> localConf = localEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value); - Dictionary<string, JsonElement> hostConf = hostEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value); - - //merge the two configs - foreach(KeyValuePair<string, JsonElement> lc in localConf) - { - //Overwrite any host config keys, plugin conf takes priority - hostConf[lc.Key] = lc.Value; - } - //set the merged config - conf = hostConf; - } - else if(local) - { - //Store only local config - conf = localEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value); - } - else if(host) - { - //store only host config - conf = hostEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value); - } - else - { - conf = null; - } - - //Get the value or default json element - return conf != null && conf.TryGetValue(secretName, out JsonElement el) ? el.GetString() : null; - } - - private static IVaultClient? TryGetVaultLoader(PluginBase pbase) - { - //Get vault config - IReadOnlyDictionary<string, JsonElement>? conf = pbase.TryGetConfig(VAULT_OBJECT_NAME); - - if (conf == null) - { - return null; - } - - //try get servre address creds from config - string? serverAddress = conf[VAULT_URL_KEY].GetString() ?? throw new KeyNotFoundException($"Failed to load the key {VAULT_URL_KEY} from object {VAULT_OBJECT_NAME}"); - - IAuthMethodInfo authMethod; - - //Get authentication method from config - if (conf.TryGetValue(VAULT_TOKEN_KEY, out JsonElement tokenEl)) - { - //Init token - authMethod = new TokenAuthMethodInfo(tokenEl.GetString()); - } - else if (conf.TryGetValue(VAULT_ROLE_KEY, out JsonElement roleEl) && conf.TryGetValue(VAULT_SECRET_KEY, out JsonElement secretEl)) - { - authMethod = new AppRoleAuthMethodInfo(roleEl.GetString(), secretEl.GetString()); - } - else - { - throw new KeyNotFoundException($"Failed to load the vault authentication method from {VAULT_OBJECT_NAME}"); - } - - //Settings - VaultClientSettings settings = new(serverAddress, authMethod); - - //create vault client - return new VaultClient(settings); - } - - /// <summary> - /// Gets the Secret value as a byte buffer - /// </summary> - /// <param name="secret"></param> - /// <returns>The base64 decoded secret as a byte[]</returns> - /// <exception cref="ArgumentNullException"></exception> - /// <exception cref="InternalBufferTooSmallException"></exception> - public static byte[] GetFromBase64(this SecretResult secret) - { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); - - //Temp buffer - using UnsafeMemoryHandle<byte> buffer = Memory.UnsafeAlloc<byte>(secret.Result.Length); - - //Get base64 - if(Convert.TryFromBase64Chars(secret.Result, buffer, out int count)) - { - //Copy to array - byte[] value = buffer.Span[..count].ToArray(); - //Clear block before returning - Memory.InitializeBlock<byte>(buffer); - - return value; - } - - throw new InternalBufferTooSmallException("internal buffer too small"); - } - - /// <summary> - /// Converts the secret recovery task to - /// </summary> - /// <param name="secret"></param> - /// <returns>A task whos result the base64 decoded secret as a byte[]</returns> - /// <exception cref="ArgumentNullException"></exception> - /// <exception cref="InternalBufferTooSmallException"></exception> - public static async Task<byte[]?> ToBase64Bytes(this Task<SecretResult?> secret) - { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); - using SecretResult? sec = await secret.ConfigureAwait(false); - return sec?.GetFromBase64(); - } - - /// <summary> - /// Recovers a certificate from a PEM encoded secret - /// </summary> - /// <param name="secret"></param> - /// <returns>The <see cref="X509Certificate2"/> parsed from the PEM encoded data</returns> - /// <exception cref="ArgumentNullException"></exception> - public static X509Certificate2 GetCertificate(this SecretResult secret) - { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); - return X509Certificate2.CreateFromPem(secret.Result); - } - - /// <summary> - /// Gets the secret value as a secret result - /// </summary> - /// <param name="secret"></param> - /// <returns>The document parsed from the secret value</returns> - public static JsonDocument GetJsonDocument(this SecretResult secret) - { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); - //Alloc buffer, utf8 so 1 byte per char - using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(secret.Result.Length); - //Get utf8 bytes - int count = Encoding.UTF8.GetBytes(secret.Result, buffer.Span); - //Reader and parse - Utf8JsonReader reader = new(buffer.Span[..count]); - return JsonDocument.ParseValue(ref reader); - } - - /// <summary> - /// Gets a SPKI encoded public key from a secret - /// </summary> - /// <param name="secret"></param> - /// <returns>The <see cref="PublicKey"/> parsed from the SPKI public key</returns> - /// <exception cref="ArgumentNullException"></exception> - public static PublicKey GetPublicKey(this SecretResult secret) - { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); - //Alloc buffer, base64 is larger than binary value so char len is large enough - using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(secret.Result.Length); - //Get base64 bytes - ERRNO count = VnEncoding.TryFromBase64Chars(secret.Result, buffer.Span); - //Parse the SPKI from base64 - return PublicKey.CreateFromSubjectPublicKeyInfo(buffer.Span[..(int)count], out _); - } - - /// <summary> - /// Gets the value of the <see cref="SecretResult"/> as a <see cref="PrivateKey"/> - /// container - /// </summary> - /// <param name="secret"></param> - /// <returns>The <see cref="PrivateKey"/> from the secret value</returns> - /// <exception cref="FormatException"></exception> - /// <exception cref="ArgumentNullException"></exception> - public static PrivateKey GetPrivateKey(this SecretResult secret) - { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); - return new PrivateKey(secret); - } - - /// <summary> - /// Gets a <see cref="ReadOnlyJsonWebKey"/> from a secret value - /// </summary> - /// <param name="secret"></param> - /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the result</returns> - /// <exception cref="JsonException"></exception> - /// <exception cref="ArgumentException"></exception> - /// <exception cref="ArgumentNullException"></exception> - public static ReadOnlyJsonWebKey GetJsonWebKey(this SecretResult secret) - { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); - //Alloc buffer, utf8 so 1 byte per char - using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(secret.Result.Length); - //Get utf8 bytes - int count = Encoding.UTF8.GetBytes(secret.Result, buffer.Span); - return new ReadOnlyJsonWebKey(buffer.Span[..count]); - } - - /// <summary> - /// Gets a task that resolves a <see cref="ReadOnlyJsonWebKey"/> - /// from a <see cref="SecretResult"/> task - /// </summary> - /// <param name="secret"></param> - /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the secret, or null if the secret was not found</returns> - /// <exception cref="ArgumentNullException"></exception> - public static async Task<ReadOnlyJsonWebKey?> ToJsonWebKey(this Task<SecretResult?> secret) - { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); - using SecretResult? sec = await secret.ConfigureAwait(false); - return sec?.GetJsonWebKey(); - } - } -} |