diff options
author | vnugent <public@vaughnnugent.com> | 2024-05-02 15:44:42 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-05-02 15:44:42 -0400 |
commit | 8e77289041349b16536497f48f0c0a4ec6fe30f5 (patch) | |
tree | a222f4b46ce48a11f0225e9edecc058e25d3f579 /lib/VNLib.Plugins.Extensions.Loading/src | |
parent | e0a5c85297516188e57b54d9b530b2482cb03eb0 (diff) |
feat: #2 Middleware helpers, proj cleanup, fix sync secrets, vault client
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Loading/src')
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/Routing/MiddlewareHelpers.cs | 70 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs | 127 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/Secrets/IKvVaultClient.cs (renamed from lib/VNLib.Plugins.Extensions.Loading/src/Secrets/IHCVaultClient.cs) | 8 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/Secrets/OnDemandSecret.cs | 2 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/Secrets/PluginSecretStore.cs | 2 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj | 12 |
6 files changed, 116 insertions, 105 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/MiddlewareHelpers.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/MiddlewareHelpers.cs new file mode 100644 index 0000000..6a0d848 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/MiddlewareHelpers.cs @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: MiddlewareHelpers.cs +* +* MiddlewareHelpers.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 Affero General Public License as +* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +using VNLib.Utils.Extensions; +using VNLib.Plugins.Essentials.Middleware; + +namespace VNLib.Plugins.Extensions.Loading.Routing +{ + /// <summary> + /// Provides helper extensions for http middleware + /// </summary> + public static class MiddlewareHelpers + { + private static readonly ConditionalWeakTable<PluginBase, List<IHttpMiddleware>> _pluginMiddlewareList = new(); + + /// <summary> + /// Exports a single middlware instance to the collection for the plugin. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="plugin"></param> + /// <param name="instances">A params array of middleware instances to export to the plugin</param> + /// <remarks> + /// WARNING: Adding middleware arrays explicitly to the plugin service pool will override + /// this function. All instances must be exposed though this function + /// </remarks> + public static void ExportMiddleware<T>(this PluginBase plugin, params T[] instances) where T : IHttpMiddleware + { + /* + * The runtime accepts an enumeration of IHttpMiddleware instances, so + * a list can just be exported as an enumerable instance + */ + static List<IHttpMiddleware> OnCreate(PluginBase plugin) + { + List<IHttpMiddleware> collection = new(1); + plugin.ExportService<IEnumerable<IHttpMiddleware>>(collection); + return collection; + } + + //Get the endpoint collection for the current plugin + List<IHttpMiddleware> middlewares = _pluginMiddlewareList.GetValue(plugin, OnCreate); + + //Add the endpoint to the collection + instances.ForEach(mw => middlewares.Add(mw)); + } + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs index a06f490..35530c0 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs @@ -30,7 +30,6 @@ using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Net.Security; -using System.Diagnostics; using System.Threading.Tasks; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -50,7 +49,12 @@ using VNLib.Utils.Extensions; namespace VNLib.Plugins.Extensions.Loading { - internal sealed class HCVaultClient : VnDisposeable, IHCVaultClient + + /// <summary> + /// A concret implementation of a Hashicorp Vault client instance used to + /// retrieve key-value secrets from a server + /// </summary> + public sealed class HCVaultClient : VnDisposeable, IKvVaultClient { const string VaultTokenHeaderName = "X-Vault-Token"; const long MaxErrResponseContentLength = 8192; @@ -62,7 +66,7 @@ namespace VNLib.Plugins.Extensions.Loading private readonly int _kvVersion; private readonly IUnmangedHeap _bufferHeap; - HCVaultClient(string serverAddress, string hcToken, int kvVersion, bool trustCert, IUnmangedHeap heap) + private HCVaultClient(string serverAddress, string hcToken, int kvVersion, bool trustCert, IUnmangedHeap heap) { #pragma warning disable CA2000 // Dispose objects before losing scope HttpClientHandler handler = new() @@ -99,17 +103,17 @@ namespace VNLib.Plugins.Extensions.Loading /// Creates a new Hashicorp vault client with the given server address, token, and KV storage version /// </summary> /// <param name="serverAddress">The vault server address</param> - /// <param name="hcToken">The vault token used to connect to the vault server</param> + /// <param name="token">The vault token used to connect to the vault server</param> /// <param name="kvVersion">The hc vault Key value store version (must be 1 or 2)</param> /// <param name="trustCert">A value that tells the HTTP client to trust the Vault server's certificate even if it's not valid</param> /// <param name="heap">Heap instance to allocate internal buffers from</param> /// <returns>The new client instance</returns> /// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentNullException"></exception> - public static HCVaultClient Create(string serverAddress, string hcToken, int kvVersion, bool trustCert, IUnmangedHeap heap) + public static HCVaultClient Create(string serverAddress, string token, int kvVersion, bool trustCert, IUnmangedHeap heap) { ArgumentException.ThrowIfNullOrEmpty(serverAddress); - ArgumentException.ThrowIfNullOrEmpty(hcToken); + ArgumentException.ThrowIfNullOrEmpty(token); ArgumentNullException.ThrowIfNull(heap); if(kvVersion != 1 && kvVersion != 2) @@ -117,7 +121,7 @@ namespace VNLib.Plugins.Extensions.Loading throw new ArgumentException($"Unsupported vault KV storage version {kvVersion}, must be either 1 or 2"); } - return new HCVaultClient(serverAddress, hcToken, kvVersion, trustCert, heap); + return new HCVaultClient(serverAddress, token, kvVersion, trustCert, heap); } ///<inheritdoc/> @@ -137,10 +141,10 @@ namespace VNLib.Plugins.Extensions.Loading using HttpResponseMessage response = await _client.SendAsync(ms, HttpCompletionOption.ResponseHeadersRead); //Check if an error occured in the response - await ProcessVaultErrorResponseAsync(response, true); + await ProcessVaultErrorResponseAsync(response); //Read the response async - using SecretResponse res = await ReadSecretResponse(response.Content, true); + using SecretResponse res = await ReadSecretResponse(response.Content); return FromResponse(res, secretName); } @@ -162,88 +166,36 @@ namespace VNLib.Plugins.Extensions.Loading ///<inheritdoc/> public ISecretResult? ReadSecret(string path, string mountPoint, string secretName) { - string secretPath = GetSecretPathForKvVersion(_kvVersion, path, mountPoint); - using HttpRequestMessage ms = GetRequestMessageForPath(secretPath); - - try - { - //Exec the response synchronously - using HttpResponseMessage response = _client.Send(ms, HttpCompletionOption.ResponseHeadersRead); - - /* - * It is safe to await the error result here because its - * already completed when the async flag is false - */ - ValueTask errTask = ProcessVaultErrorResponseAsync(response, false); - Debug.Assert(errTask.IsCompleted); - errTask.GetAwaiter().GetResult(); - - //Did not throw, handle a secret response - - ValueTask<SecretResponse> resTask = ReadSecretResponse(response.Content, false); - Debug.Assert(resTask.IsCompleted); + /* + * Since this method will syncrhonously block the calling thread, a new + * task must be created to ignore the current async context and run the + * funciton in an new context to block safely without causing a deadlock. + */ - //Always wrap response in using to clean memory - using SecretResponse res = resTask.GetAwaiter().GetResult(); + Task<ISecretResult?> asAsync = Task.Run(() => ReadSecretAsync(path, mountPoint, secretName)); + + asAsync.Wait(ClientDefaultTimeout); - return FromResponse(res, secretName); - } - catch (HttpRequestException he) when (he.InnerException is SocketException se) - { - throw se.SocketErrorCode switch - { - SocketError.HostNotFound => new HCVaultException("Failed to connect to Hashicorp Vault server, because it's DNS hostname could not be resolved"), - SocketError.ConnectionRefused => new HCVaultException("Failed to establish a TCP connection to the vault server, the server refused the connection"), - _ => new HCVaultException("Failed to establish a TCP connection to the vault server, see inner exception", se), - }; - } - catch (Exception ex) - { - throw new HCVaultException("Failed to retreive secret from Hashicorp Vault server, see inner exception", ex); - } + return asAsync.Result; } - private ValueTask<SecretResponse> ReadSecretResponse(HttpContent content, bool async) + private async Task<SecretResponse> ReadSecretResponse(HttpContent content) { - SecretResponse res = new(DefaultBufferSize, _bufferHeap); + SecretResponse response = new(DefaultBufferSize, _bufferHeap); + try { - if (async) - { - return ReadStreamAsync(content, res); - } - else - { - //Read into a memory stream - content.CopyTo(res.StreamData, null, default); - res.ResetStream(); + await content.CopyToAsync(response.StreamData); - return ValueTask.FromResult(res); - } + response.ResetStream(); + + return response; } catch { - res.Dispose(); + response.Dispose(); throw; } - - async static ValueTask<SecretResponse> ReadStreamAsync(HttpContent content, SecretResponse response) - { - try - { - await content.CopyToAsync(response.StreamData); - - response.ResetStream(); - - return response; - } - catch - { - response.Dispose(); - throw; - } - } - } private static string GetSecretPathForKvVersion(int version, string path, string mount) @@ -288,7 +240,7 @@ namespace VNLib.Plugins.Extensions.Loading return null; } - private static ValueTask ProcessVaultErrorResponseAsync(HttpResponseMessage response, bool async) + private static ValueTask ProcessVaultErrorResponseAsync(HttpResponseMessage response) { if (response.IsSuccessStatusCode) { @@ -322,12 +274,12 @@ namespace VNLib.Plugins.Extensions.Loading ); } - return async ? ExceptionsFromContentAsync(response) : ExceptionsFromContent(response); + return ExceptionsFromContentAsync(response); static ValueTask ExceptionFromVaultErrors(HttpStatusCode code, VaultErrorMessage? errs) { //If the error message is null, raise an exception - if (errs == null || errs.Errors == null || errs.Errors.Length == 0) + if (errs?.Errors is null || errs.Errors.Length == 0) { return ValueTask.FromException( new HttpRequestException($"Failed to fetch secret from vault with error code {code}") @@ -352,19 +304,6 @@ namespace VNLib.Plugins.Extensions.Loading await ExceptionFromVaultErrors(response.StatusCode, errs); } - - static ValueTask ExceptionsFromContent(HttpResponseMessage response) - { -#pragma warning disable CA1849 // Call async methods when in an async method - - //Read the error content stream and deserialize - using Stream stream = response.Content.ReadAsStream(); - VaultErrorMessage? errs = JsonSerializer.Deserialize<VaultErrorMessage>(stream); - -#pragma warning restore CA1849 // Call async methods when in an async method - - return ExceptionFromVaultErrors(response.StatusCode, errs); - } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/IHCVaultClient.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/IKvVaultClient.cs index aab2541..876d8b6 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/IHCVaultClient.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/IKvVaultClient.cs @@ -3,9 +3,9 @@ * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading -* File: IHCVaultClient.cs +* File: ISecretVaultClient.cs * -* IHCVaultClient.cs is part of VNLib.Plugins.Extensions.Loading which is +* ISecretVaultClient.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 @@ -29,9 +29,9 @@ using System.Threading.Tasks; namespace VNLib.Plugins.Extensions.Loading { /// <summary> - /// A Hashicorp Vault client for reading secrets from a vault server + /// A secret client interace for reading secrets from a vault server /// </summary> - public interface IHCVaultClient + public interface IKvVaultClient { /// <summary> /// Reads a single KeyValue secret from the vault server asyncrhonously and returns the result diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/OnDemandSecret.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/OnDemandSecret.cs index 6e6d560..17f3523 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/OnDemandSecret.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/OnDemandSecret.cs @@ -40,7 +40,7 @@ using static VNLib.Plugins.Extensions.Loading.PluginSecretConstants; namespace VNLib.Plugins.Extensions.Loading { - internal sealed class OnDemandSecret(PluginBase plugin, string secretName, IHCVaultClient? vault) : IOnDemandSecret + internal sealed class OnDemandSecret(PluginBase plugin, string secretName, IKvVaultClient? vault) : IOnDemandSecret { public string SecretName { get; } = secretName ?? throw new ArgumentNullException(nameof(secretName)); diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/PluginSecretStore.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/PluginSecretStore.cs index 6b20e30..1d366b0 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/PluginSecretStore.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/PluginSecretStore.cs @@ -49,7 +49,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <returns>The ambient <see cref="IVaultClient"/> if loaded, null otherwise</returns> /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> - public IHCVaultClient? GetVaultClient() => LoadingExtensions.GetOrCreateSingleton(_plugin, TryGetVaultLoader); + public IKvVaultClient? GetVaultClient() => LoadingExtensions.GetOrCreateSingleton(_plugin, TryGetVaultLoader); private static HCVaultClient? TryGetVaultLoader(PluginBase pbase) { diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj b/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj index be21770..8fa72fb 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj +++ b/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj @@ -2,13 +2,16 @@ <PropertyGroup> <TargetFramework>net8.0</TargetFramework> + <Nullable>enable</Nullable> <RootNamespace>VNLib.Plugins.Extensions.Loading</RootNamespace> <AssemblyName>VNLib.Plugins.Extensions.Loading</AssemblyName> <GenerateDocumentationFile>True</GenerateDocumentationFile> - <Nullable>enable</Nullable> - <AnalysisLevel>latest-all</AnalysisLevel> <PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance> </PropertyGroup> + + <PropertyGroup> + <AnalysisLevel Condition="'$(BuildingInsideVisualStudio)' == true">latest-all</AnalysisLevel> + </PropertyGroup> <PropertyGroup> <Authors>Vaughn Nugent</Authors> @@ -19,12 +22,11 @@ <Copyright>Copyright © 2024 Vaughn Nugent</Copyright> <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions</PackageProjectUrl> <RepositoryUrl>https://github.com/VnUgE/VNLib.Plugins.Extensions/tree/master/lib/VNLib.Plugins.Extensions.Loading</RepositoryUrl> - </PropertyGroup> - - <PropertyGroup> <PackageReadmeFile>README.md</PackageReadmeFile> <PackageLicenseFile>LICENSE</PackageLicenseFile> + <PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance> </PropertyGroup> + <ItemGroup> <None Include="..\..\..\LICENSE"> <Pack>True</Pack> |