From 5ddef0fcb742e77b99a0e17015d2eea0a1d4131a Mon Sep 17 00:00:00 2001 From: vnugent Date: Thu, 9 Mar 2023 01:48:28 -0500 Subject: Omega cache, session, and account provider complete overhaul --- lib/Hashing.Portable/src/Argon2/VnArgon2.cs | 23 +- .../src/IdentityUtility/IJsonWebKey.cs | 50 ++ .../src/IdentityUtility/JsonWebKey.cs | 193 ++--- .../src/IdentityUtility/ReadOnlyJsonWebKey.cs | 39 +- lib/Net.Http/src/Core/ConnectionInfo.cs | 2 + .../src/HttpServiceStack.cs | 33 +- .../src/HttpServiceStackBuilder.cs | 6 +- .../src/IPluginController.cs | 28 +- .../src/IPluginWrapper.cs | 54 ++ .../src/IServiceHost.cs | 32 +- .../src/IUnloadableServiceProvider.cs | 42 ++ .../src/ManagedPlugin.cs | 202 ++++++ .../src/PluginLoadConfiguration.cs | 62 ++ .../src/PluginManager.cs | 240 ++++++ .../src/ServiceDomain.cs | 284 +------- .../src/ServiceGroup.cs | 76 +- .../VNLib.Plugins.Essentials.ServiceStack.csproj | 1 - .../src/Accounts/AccountUtils.cs | 807 ++++++--------------- .../src/Accounts/AuthorzationCheckLevel.cs | 56 ++ .../src/Accounts/ClientSecurityToken.cs | 43 ++ .../src/Accounts/IAccountSecurityProvider.cs | 90 +++ .../src/Accounts/IClientAuthorization.cs | 45 ++ .../src/Accounts/IClientSecInfo.cs | 46 ++ .../src/Accounts/IPasswordHashingProvider.cs | 80 ++ .../src/Accounts/LoginMessage.cs | 12 +- .../src/Accounts/PasswordChallengeResult.cs | 38 + .../src/Accounts/PasswordHashing.cs | 157 ++-- .../src/Endpoints/ProtectedWebEndpoint.cs | 14 +- lib/Plugins.Essentials/src/EventProcessor.cs | 35 +- .../src/Extensions/EssentialHttpEventExtensions.cs | 12 +- .../src/Extensions/HttpCookie.cs | 26 +- lib/Plugins.Essentials/src/HttpEntity.cs | 4 +- lib/Plugins.Essentials/src/IEpProcessingOptions.cs | 8 +- lib/Plugins.Essentials/src/IWebProcessorInfo.cs | 82 +++ .../src/Oauth/OauthHttpExtensions.cs | 137 +--- lib/Plugins.Essentials/src/Sessions/SessionBase.cs | 20 +- lib/Plugins.Essentials/src/Sessions/SessionInfo.cs | 165 +++-- lib/Plugins.Runtime/src/IPluginEventListener.cs | 49 ++ lib/Plugins.Runtime/src/IPluginEventRegistrar.cs | 47 ++ lib/Plugins.Runtime/src/ITypedPluginConsumer.cs | 47 ++ lib/Plugins.Runtime/src/LivePlugin.cs | 66 +- lib/Plugins.Runtime/src/LoaderExtensions.cs | 174 +++-- lib/Plugins.Runtime/src/PluginController.cs | 127 ++++ lib/Plugins.Runtime/src/PluginEventRegistration.cs | 69 ++ lib/Plugins.Runtime/src/RuntimePluginLoader.cs | 201 +++-- .../src/VNLib.Plugins.Runtime.csproj | 3 +- lib/Utils/src/Extensions/JsonExtensions.cs | 27 +- lib/Utils/src/VnEncoding.cs | 64 +- 48 files changed, 2513 insertions(+), 1605 deletions(-) create mode 100644 lib/Hashing.Portable/src/IdentityUtility/IJsonWebKey.cs create mode 100644 lib/Plugins.Essentials.ServiceStack/src/IPluginWrapper.cs create mode 100644 lib/Plugins.Essentials.ServiceStack/src/IUnloadableServiceProvider.cs create mode 100644 lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs create mode 100644 lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs create mode 100644 lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/AuthorzationCheckLevel.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/ClientSecurityToken.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/IAccountSecurityProvider.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/IClientAuthorization.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/IClientSecInfo.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/IPasswordHashingProvider.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/PasswordChallengeResult.cs create mode 100644 lib/Plugins.Essentials/src/IWebProcessorInfo.cs create mode 100644 lib/Plugins.Runtime/src/IPluginEventListener.cs create mode 100644 lib/Plugins.Runtime/src/IPluginEventRegistrar.cs create mode 100644 lib/Plugins.Runtime/src/ITypedPluginConsumer.cs create mode 100644 lib/Plugins.Runtime/src/PluginController.cs create mode 100644 lib/Plugins.Runtime/src/PluginEventRegistration.cs diff --git a/lib/Hashing.Portable/src/Argon2/VnArgon2.cs b/lib/Hashing.Portable/src/Argon2/VnArgon2.cs index 5963ca7..519d35f 100644 --- a/lib/Hashing.Portable/src/Argon2/VnArgon2.cs +++ b/lib/Hashing.Portable/src/Argon2/VnArgon2.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Hashing.Portable @@ -25,6 +25,7 @@ using System; using System.Text; +using System.Buffers; using System.Threading; using System.Buffers.Text; using System.Security.Cryptography; @@ -142,7 +143,7 @@ namespace VNLib.Hashing int passBytes = LocEncoding.GetByteCount(password); //Alloc memory for salt - using MemoryHandle buffer = PwHeap.Alloc(saltbytes + passBytes, true); + using IMemoryHandle buffer = PwHeap.Alloc(saltbytes + passBytes, true); Span saltBuffer = buffer.AsSpan(0, saltbytes); Span passBuffer = buffer.AsSpan(passBytes); @@ -178,10 +179,10 @@ namespace VNLib.Hashing int passBytes = LocEncoding.GetByteCount(password); //Alloc memory for password - using MemoryHandle pwdHandle = PwHeap.Alloc(passBytes, true); + using IMemoryHandle pwdHandle = PwHeap.Alloc(passBytes, true); //Encode password, create a new span to make sure its proper size - _ = LocEncoding.GetBytes(password, pwdHandle); + _ = LocEncoding.GetBytes(password, pwdHandle.Span); //Hash return Hash2id(pwdHandle.Span, salt, secret, timeCost, memCost, parallelism, hashLen); @@ -205,7 +206,7 @@ namespace VNLib.Hashing { string hash, salts; //Alloc data for hash output - using MemoryHandle hashHandle = PwHeap.Alloc(hashLen, true); + using IMemoryHandle hashHandle = PwHeap.Alloc(hashLen, true); //hash the password Hash2id(password, salt, secret, hashHandle.Span, timeCost, memCost, parallelism); @@ -219,6 +220,7 @@ namespace VNLib.Hashing //Encode salt in base64 return $"${ID_MODE}$v={(int)Argon2_version.VERSION_13},m={memCost},t={timeCost},p={parallelism},s={salts}${hash}"; } + /// /// Exposes the raw Argon2-ID hashing api to C#, using spans (pins memory references) @@ -231,7 +233,7 @@ namespace VNLib.Hashing /// Degree of parallelism /// Time cost of operation /// - public static void Hash2id(ReadOnlySpan password, ReadOnlySpan salt, ReadOnlySpan secret, in Span rawHashOutput, + public static void Hash2id(ReadOnlySpan password, ReadOnlySpan salt, ReadOnlySpan secret, Span rawHashOutput, uint timeCost = 2, uint memCost = 65535, uint parallelism = 4) { fixed (byte* pwd = password, slptr = salt, secretptr = secret, outPtr = rawHashOutput) @@ -289,7 +291,10 @@ namespace VNLib.Hashing uint timeCost = 2, uint memCost = 65535, uint parallelism = 4) { //Alloc data for hash output - using MemoryHandle outputHandle = PwHeap.Alloc(hashBytes.Length, true); + using IMemoryHandle outputHandle = PwHeap.Alloc(hashBytes.Length, true); + + //Pin to get the base pointer + using MemoryHandle outputPtr = outputHandle.Pin(0); //Get pointers fixed (byte* secretptr = secret, pwd = rawPass, slptr = salt) @@ -318,7 +323,7 @@ namespace VNLib.Hashing context->secret = secretptr; context->secretlen = (uint)secret.Length; //Output - context->outptr = outputHandle.Base; + context->outptr = outputPtr.Pointer; context->outlen = (uint)outputHandle.Length; //Hash Argon2_ErrorCodes result = (Argon2_ErrorCodes)_nativeLibrary.Value.Argon2Hash(&ctx); @@ -365,7 +370,7 @@ namespace VNLib.Hashing int rawPassLen = LocEncoding.GetByteCount(rawPass); //Alloc buffer for decoded data - using MemoryHandle rawBufferHandle = MemoryUtil.Shared.Alloc(passBase64BufSize + saltBase64BufSize + rawPassLen, true); + using IMemoryHandle rawBufferHandle = MemoryUtil.Shared.Alloc(passBase64BufSize + saltBase64BufSize + rawPassLen, true); //Split buffers Span saltBuf = rawBufferHandle.Span[..saltBase64BufSize]; diff --git a/lib/Hashing.Portable/src/IdentityUtility/IJsonWebKey.cs b/lib/Hashing.Portable/src/IdentityUtility/IJsonWebKey.cs new file mode 100644 index 0000000..8e0f253 --- /dev/null +++ b/lib/Hashing.Portable/src/IdentityUtility/IJsonWebKey.cs @@ -0,0 +1,50 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Hashing.Portable +* File: IJsonWebKey.cs +* +* IJsonWebKey.cs is part of VNLib.Hashing.Portable which is part +* of the larger VNLib collection of libraries and utilities. +* +* VNLib.Hashing.Portable 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.Hashing.Portable 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.Hashing.Portable. If not, see http://www.gnu.org/licenses/. +*/ + +namespace VNLib.Hashing.IdentityUtility +{ + /// + /// An abstraction for basic JsonWebKey operations + /// + public interface IJsonWebKey + { + /// + /// The key usage, may be Siganture, or Encryption + /// + JwkKeyUsage KeyUse { get; } + + /// + /// The cryptographic algorithm this key is to be used for + /// + string Algorithm { get; } + + /// + /// Gets miscelaneous key properties on demand. May return null results if the property + /// is not defined in the current key + /// + /// The name of the key property to get + /// The value at the key property + string? GetKeyProperty(string propertyName); + } +} diff --git a/lib/Hashing.Portable/src/IdentityUtility/JsonWebKey.cs b/lib/Hashing.Portable/src/IdentityUtility/JsonWebKey.cs index 9076e5b..e8bd13f 100644 --- a/lib/Hashing.Portable/src/IdentityUtility/JsonWebKey.cs +++ b/lib/Hashing.Portable/src/IdentityUtility/JsonWebKey.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Hashing.Portable @@ -57,9 +57,32 @@ namespace VNLib.Hashing.IdentityUtility {} } + /// + /// The JWK key usage flags + /// + public enum JwkKeyUsage + { + /// + /// Default/not supported operation + /// + None, + /// + /// The key supports cryptographic signatures + /// + Signature, + /// + /// The key supports encryption operations + /// + Encryption + } + + /// + /// Contains extension methods for verifying and signing + /// using s. + /// public static class JsonWebKey - { - + { + /// /// Verifies the against the supplied /// Json Web Key in format @@ -70,81 +93,73 @@ namespace VNLib.Hashing.IdentityUtility /// /// /// - public static bool VerifyFromJwk(this JsonWebToken token, in JsonElement jwk) - { - //Get key use and algorithm - string? use = jwk.GetPropString("use"); - string? alg = jwk.GetPropString("alg"); - + public static bool VerifyFromJwk(this JsonWebToken token, in TKey jwk) where TKey: notnull, IJsonWebKey + { //Use and alg are required here - if(use == null || alg == null) - { - return false; - } - - //Make sure the key is used for signing/verification - if (!"sig".Equals(use, StringComparison.OrdinalIgnoreCase)) + if(jwk.KeyUse != JwkKeyUsage.Signature || jwk.Algorithm == null) { return false; } + //Get the jwt header to confirm its the same algorithm as the jwk using (JsonDocument jwtHeader = token.GetHeader()) { string? jwtAlg = jwtHeader.RootElement.GetPropString("alg"); + //Make sure the jwt was signed with the same algorithm type - if (!alg.Equals(jwtAlg, StringComparison.OrdinalIgnoreCase)) + if (!jwk.Algorithm.Equals(jwtAlg, StringComparison.OrdinalIgnoreCase)) { return false; } } - switch (alg.ToUpper(null)) + switch (jwk.Algorithm.ToUpper(null)) { //Rsa witj pkcs and pss case JWKAlgorithms.RS256: { - using RSA? rsa = GetRSAPublicKey(in jwk); + using RSA? rsa = GetRSAPublicKey(jwk); return rsa != null && token.Verify(rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } case JWKAlgorithms.RS384: { - using RSA? rsa = GetRSAPublicKey(in jwk); + using RSA? rsa = GetRSAPublicKey(jwk); return rsa != null && token.Verify(rsa, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1); } case JWKAlgorithms.RS512: { - using RSA? rsa = GetRSAPublicKey(in jwk); + using RSA? rsa = GetRSAPublicKey(jwk); return rsa != null && token.Verify(rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); } case JWKAlgorithms.PS256: { - using RSA? rsa = GetRSAPublicKey(in jwk); + using RSA? rsa = GetRSAPublicKey(jwk); return rsa != null && token.Verify(rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); } case JWKAlgorithms.PS384: { - using RSA? rsa = GetRSAPublicKey(in jwk); + using RSA? rsa = GetRSAPublicKey(jwk); return rsa != null && token.Verify(rsa, HashAlgorithmName.SHA384, RSASignaturePadding.Pss); } case JWKAlgorithms.PS512: { - using RSA? rsa = GetRSAPublicKey(in jwk); + using RSA? rsa = GetRSAPublicKey(jwk); return rsa != null && token.Verify(rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pss); } //Eccurves case JWKAlgorithms.ES256: { - using ECDsa? eCDsa = GetECDsaPublicKey(in jwk); + using ECDsa? eCDsa = GetECDsaPublicKey(jwk); return eCDsa != null && token.Verify(eCDsa, HashAlgorithmName.SHA256); } case JWKAlgorithms.ES384: { - using ECDsa? eCDsa = GetECDsaPublicKey(in jwk); + using ECDsa? eCDsa = GetECDsaPublicKey(jwk); return eCDsa != null && token.Verify(eCDsa, HashAlgorithmName.SHA384); } case JWKAlgorithms.ES512: { - using ECDsa? eCDsa = GetECDsaPublicKey(in jwk); + using ECDsa? eCDsa = GetECDsaPublicKey(jwk); return eCDsa != null && token.Verify(eCDsa, HashAlgorithmName.SHA512); } default: @@ -152,18 +167,6 @@ namespace VNLib.Hashing.IdentityUtility } } - - /// - /// Verifies the against the supplied - /// - /// - /// - /// The supplied single Json Web Key - /// True if required JWK data exists, ciphers were created, and data is verified, false otherwise - /// - /// - /// - public static bool VerifyFromJwk(this JsonWebToken token, ReadOnlyJsonWebKey jwk) => token.VerifyFromJwk(jwk.KeyElement); /// /// Signs the with the supplied JWK json element @@ -173,66 +176,63 @@ namespace VNLib.Hashing.IdentityUtility /// /// /// - public static void SignFromJwk(this JsonWebToken token, in JsonElement jwk) + public static void SignFromJwk(this JsonWebToken token, in T jwk) where T: notnull, IJsonWebKey { _ = token ?? throw new ArgumentNullException(nameof(token)); - //Get key use and algorithm - string? use = jwk.GetPropString("use"); - string? alg = jwk.GetPropString("alg"); - //Use and alg are required here - if (use == null || alg == null) + //Make sure the key is used for signing/verification + if (jwk.KeyUse != JwkKeyUsage.Signature) { - throw new InvalidOperationException("Algorithm or JWK use is null"); + throw new InvalidOperationException("The JWK cannot be used for signing"); } - //Make sure the key is used for signing/verification - if (!"sig".Equals(use, StringComparison.OrdinalIgnoreCase)) + //Alg is a required property + if (jwk.Algorithm == null) { - throw new InvalidOperationException("The JWK cannot be used for signing"); + throw new InvalidOperationException("Algorithm or JWK use is null"); } - switch (alg.ToUpper(null)) + switch (jwk.Algorithm.ToUpper(null)) { //Rsa witj pkcs and pss case JWKAlgorithms.RS256: { - using RSA? rsa = GetRSAPrivateKey(in jwk); + using RSA? rsa = GetRSAPrivateKey(jwk); _ = rsa ?? throw new InvalidOperationException("JWK Does not contain an RSA private key"); token.Sign(rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1, 128); return; } case JWKAlgorithms.RS384: { - using RSA? rsa = GetRSAPrivateKey(in jwk); + using RSA? rsa = GetRSAPrivateKey(jwk); _ = rsa ?? throw new InvalidOperationException("JWK Does not contain an RSA private key"); token.Sign(rsa, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1, 128); return; } case JWKAlgorithms.RS512: { - using RSA? rsa = GetRSAPrivateKey(in jwk); + using RSA? rsa = GetRSAPrivateKey(jwk); _ = rsa ?? throw new InvalidOperationException("JWK Does not contain an RSA private key"); token.Sign(rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1, 256); return; } case JWKAlgorithms.PS256: { - using RSA? rsa = GetRSAPrivateKey(in jwk); + using RSA? rsa = GetRSAPrivateKey(jwk); _ = rsa ?? throw new InvalidOperationException("JWK Does not contain an RSA private key"); token.Sign(rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pss, 128); return; } case JWKAlgorithms.PS384: { - using RSA? rsa = GetRSAPrivateKey(in jwk); + using RSA? rsa = GetRSAPrivateKey(jwk); _ = rsa ?? throw new InvalidOperationException("JWK Does not contain an RSA private key"); token.Sign(rsa, HashAlgorithmName.SHA384, RSASignaturePadding.Pss, 128); return; } case JWKAlgorithms.PS512: { - using RSA? rsa = GetRSAPrivateKey(in jwk); + using RSA? rsa = GetRSAPrivateKey(jwk); _ = rsa ?? throw new InvalidOperationException("JWK Does not contain an RSA private key"); token.Sign(rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pss, 256); return; @@ -240,21 +240,21 @@ namespace VNLib.Hashing.IdentityUtility //Eccurves case JWKAlgorithms.ES256: { - using ECDsa? eCDsa = GetECDsaPrivateKey(in jwk); + using ECDsa? eCDsa = GetECDsaPrivateKey(jwk); _ = eCDsa ?? throw new InvalidOperationException("JWK Does not contain an ECDsa private key"); token.Sign(eCDsa, HashAlgorithmName.SHA256, 128); return; } case JWKAlgorithms.ES384: { - using ECDsa? eCDsa = GetECDsaPrivateKey(in jwk); + using ECDsa? eCDsa = GetECDsaPrivateKey(jwk); _ = eCDsa ?? throw new InvalidOperationException("JWK Does not contain an ECDsa private key"); token.Sign(eCDsa, HashAlgorithmName.SHA384, 128); return; } case JWKAlgorithms.ES512: { - using ECDsa? eCDsa = GetECDsaPrivateKey(in jwk); + using ECDsa? eCDsa = GetECDsaPrivateKey(jwk); _ = eCDsa ?? throw new InvalidOperationException("JWK Does not contain an ECDsa private key"); token.Sign(eCDsa, HashAlgorithmName.SHA512, 256); return; @@ -264,36 +264,12 @@ namespace VNLib.Hashing.IdentityUtility } } - /// - /// Signs the with the supplied JWK json element - /// - /// - /// The JWK in the - /// - /// - /// - public static void SignFromJwk(this JsonWebToken token, ReadOnlyJsonWebKey jwk) => token.SignFromJwk(jwk.KeyElement); - - /// - /// Gets the public key algorithm for the current - /// - /// - /// The algorithm of the public key if loaded - public static RSA? GetRSAPublicKey(this ReadOnlyJsonWebKey key) => key == null ? null : GetRSAPublicKey(key.KeyElement); - - /// - /// Gets the private key algorithm for the current - /// - /// - ///The algorithm of the private key key if loaded - public static RSA? GetRSAPrivateKey(this ReadOnlyJsonWebKey key) => key == null ? null : GetRSAPrivateKey(key.KeyElement); - /// /// Gets the RSA public key algorithm from the supplied Json Web Key /// /// The element that contains the JWK data /// The algorithm if found, or null if the element does not contain public key - public static RSA? GetRSAPublicKey(in JsonElement jwk) + public static RSA? GetRSAPublicKey(this TKey jwk) where TKey: IJsonWebKey { RSAParameters? rSAParameters = GetRsaParameters(in jwk, false); //Create rsa from params @@ -305,18 +281,18 @@ namespace VNLib.Hashing.IdentityUtility /// /// /// The algorithm if found, or null if the element does not contain private key - public static RSA? GetRSAPrivateKey(in JsonElement jwk) + public static RSA? GetRSAPrivateKey(this TKey jwk) where TKey: IJsonWebKey { RSAParameters? rSAParameters = GetRsaParameters(in jwk, true); //Create rsa from params return rSAParameters.HasValue ? RSA.Create(rSAParameters.Value) : null; } - private static RSAParameters? GetRsaParameters(in JsonElement jwk, bool includePrivateKey) + private static RSAParameters? GetRsaParameters(in TKey jwk, bool includePrivateKey) where TKey : IJsonWebKey { //Get the RSA public key credentials - ReadOnlySpan e = jwk.GetPropString("e"); - ReadOnlySpan n = jwk.GetPropString("n"); + ReadOnlySpan e = jwk.GetKeyProperty("e"); + ReadOnlySpan n = jwk.GetKeyProperty("n"); if (e.IsEmpty || n.IsEmpty) { @@ -326,11 +302,11 @@ namespace VNLib.Hashing.IdentityUtility if (includePrivateKey) { //Get optional private key params - ReadOnlySpan d = jwk.GetPropString("d"); - ReadOnlySpan dp = jwk.GetPropString("dq"); - ReadOnlySpan dq = jwk.GetPropString("dp"); - ReadOnlySpan p = jwk.GetPropString("p"); - ReadOnlySpan q = jwk.GetPropString("q"); + ReadOnlySpan d = jwk.GetKeyProperty("d"); + ReadOnlySpan dp = jwk.GetKeyProperty("dq"); + ReadOnlySpan dq = jwk.GetKeyProperty("dp"); + ReadOnlySpan p = jwk.GetKeyProperty("p"); + ReadOnlySpan q = jwk.GetKeyProperty("q"); //Create params from exponent, moduls and private key components return new() @@ -353,30 +329,15 @@ namespace VNLib.Hashing.IdentityUtility Modulus = FromBase64UrlChars(n), }; } - } - - - /// - /// Gets the ECDsa public key algorithm for the current - /// - /// - /// The algorithm of the public key if loaded - public static ECDsa? GetECDsaPublicKey(this ReadOnlyJsonWebKey key) => key == null ? null : GetECDsaPublicKey(key.KeyElement); - - /// - /// Gets the private key algorithm for the current - /// - /// - ///The algorithm of the private key key if loaded - public static ECDsa? GetECDsaPrivateKey(this ReadOnlyJsonWebKey key) => key == null ? null : GetECDsaPrivateKey(key.KeyElement); + /// /// Gets the ECDsa public key algorithm from the supplied Json Web Key /// /// The public key element /// The algorithm from the key if loaded, null if no key data was found - public static ECDsa? GetECDsaPublicKey(in JsonElement jwk) + public static ECDsa? GetECDsaPublicKey(this TKey jwk) where TKey: IJsonWebKey { //Get the EC params ECParameters? ecParams = GetECParameters(in jwk, false); @@ -389,7 +350,7 @@ namespace VNLib.Hashing.IdentityUtility /// /// The element that contains the private key data /// The algorithm from the key if loaded, null if no key data was found - public static ECDsa? GetECDsaPrivateKey(in JsonElement jwk) + public static ECDsa? GetECDsaPrivateKey(this TKey jwk) where TKey : IJsonWebKey { //Get the EC params ECParameters? ecParams = GetECParameters(in jwk, true); @@ -398,14 +359,14 @@ namespace VNLib.Hashing.IdentityUtility } - private static ECParameters? GetECParameters(in JsonElement jwk, bool includePrivate) + private static ECParameters? GetECParameters(in TKey jwk, bool includePrivate) where TKey : IJsonWebKey { //Get the RSA public key credentials - ReadOnlySpan x = jwk.GetPropString("x"); - ReadOnlySpan y = jwk.GetPropString("y"); + ReadOnlySpan x = jwk.GetKeyProperty("x"); + ReadOnlySpan y = jwk.GetKeyProperty("y"); //Optional private key - ReadOnlySpan d = includePrivate ? jwk.GetPropString("d") : null; + ReadOnlySpan d = includePrivate ? jwk.GetKeyProperty("d") : null; if (x.IsEmpty || y.IsEmpty) { @@ -414,7 +375,7 @@ namespace VNLib.Hashing.IdentityUtility ECCurve curve; //Get the EC curve name from the curve ID - switch (jwk.GetPropString("crv")?.ToUpper(null)) + switch (jwk.GetKeyProperty("crv")?.ToUpper(null)) { case "P-256": curve = ECCurve.NamedCurves.nistP256; diff --git a/lib/Hashing.Portable/src/IdentityUtility/ReadOnlyJsonWebKey.cs b/lib/Hashing.Portable/src/IdentityUtility/ReadOnlyJsonWebKey.cs index f86855a..a50836f 100644 --- a/lib/Hashing.Portable/src/IdentityUtility/ReadOnlyJsonWebKey.cs +++ b/lib/Hashing.Portable/src/IdentityUtility/ReadOnlyJsonWebKey.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Hashing.Portable @@ -24,10 +24,10 @@ using System; using System.Text.Json; +using System.Collections.Generic; using VNLib.Utils; using VNLib.Utils.Extensions; -using System.Collections.Generic; namespace VNLib.Hashing.IdentityUtility { @@ -35,10 +35,10 @@ namespace VNLib.Hashing.IdentityUtility /// A readonly Json Web Key (JWK) data structure that may be used for signing /// or verifying messages. /// - public sealed class ReadOnlyJsonWebKey : VnDisposeable + public sealed class ReadOnlyJsonWebKey : VnDisposeable, IJsonWebKey { private readonly JsonElement _jwk; - private readonly JsonDocument? doc; + private readonly JsonDocument? _doc; /// /// Creates a new instance of from a . @@ -60,6 +60,14 @@ namespace VNLib.Hashing.IdentityUtility { "alg" , Algorithm }, { "typ" , "JWT" }, }; + + //Configure key usage + KeyUse = (Use?.ToLower(null)) switch + { + "sig" => JwkKeyUsage.Signature, + "enc" => JwkKeyUsage.Encryption, + _ => JwkKeyUsage.None, + }; } /// @@ -73,9 +81,9 @@ namespace VNLib.Hashing.IdentityUtility { //Pare the raw value Utf8JsonReader reader = new (rawValue); - doc = JsonDocument.ParseValue(ref reader); + _doc = JsonDocument.ParseValue(ref reader); //store element - _jwk = doc.RootElement; + _jwk = _doc.RootElement; //Set initial values KeyId = _jwk.GetPropString("kid"); @@ -89,6 +97,14 @@ namespace VNLib.Hashing.IdentityUtility { "alg" , Algorithm }, { "typ" , "JWT" }, }; + + //Configure key usage + KeyUse = (Use?.ToLower(null)) switch + { + "sig" => JwkKeyUsage.Signature, + "enc" => JwkKeyUsage.Encryption, + _ => JwkKeyUsage.None, + }; } /// @@ -112,16 +128,17 @@ namespace VNLib.Hashing.IdentityUtility /// Returns the JWT header that matches this key /// public IReadOnlyDictionary JwtHeader { get; } + + /// + public JwkKeyUsage KeyUse { get; } - /// - /// The key element - /// - internal JsonElement KeyElement => _jwk; + /// + public string? GetKeyProperty(string propertyName) => _jwk.GetPropString(propertyName); /// protected override void Free() { - doc?.Dispose(); + _doc?.Dispose(); } } diff --git a/lib/Net.Http/src/Core/ConnectionInfo.cs b/lib/Net.Http/src/Core/ConnectionInfo.cs index 6e1660d..d193467 100644 --- a/lib/Net.Http/src/Core/ConnectionInfo.cs +++ b/lib/Net.Http/src/Core/ConnectionInfo.cs @@ -113,6 +113,7 @@ namespace VNLib.Net.Http }).Any(); return accepted; } + /// /// Determines if the connection accepts any content type /// @@ -122,6 +123,7 @@ namespace VNLib.Net.Http //Accept any if no accept header was present, or accept all value */* return Context.Request.Accept.Count == 0 || Accept.Where(static t => t.StartsWith("*/*", StringComparison.OrdinalIgnoreCase)).Any(); } + /// public void SetCookie(string name, string value, string? domain, string? path, TimeSpan Expires, CookieSameSite sameSite, bool httpOnly, bool secure) { diff --git a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs index 5800955..45282b3 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -22,15 +22,20 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + using VNLib.Utils; using VNLib.Net.Http; namespace VNLib.Plugins.Essentials.ServiceStack { /// - /// The service domain controller that manages all - /// servers for an application based on a - /// + /// An HTTP servicing stack that manages a collection of HTTP servers + /// their service domain /// public sealed class HttpServiceStack : VnDisposeable { @@ -48,7 +53,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// /// The service domain's plugin controller /// - public IPluginController PluginController => _serviceDomain; + public IPluginManager PluginManager => _serviceDomain.PluginManager; /// /// Initializes a new that will @@ -74,14 +79,14 @@ namespace VNLib.Plugins.Essentials.ServiceStack //Init new linked cts to stop all servers if cancelled _cts = CancellationTokenSource.CreateLinkedTokenSource(parentToken); - LinkedList runners = new(); + //Start all servers + Task[] runners = _servers.Select(s => s.Start(_cts.Token)).ToArray(); - foreach(HttpServer server in _servers) - { - //Start servers and add run task to list - Task run = server.Start(_cts.Token); - runners.AddLast(run); - } + //Check for failed startups + Task? firstFault = runners.Where(static t => t.IsFaulted).FirstOrDefault(); + + //Raise first exception + firstFault?.GetAwaiter().GetResult(); //Task that waits for all to exit then cleans up WaitForAllTask = Task.WhenAll(runners) @@ -96,6 +101,8 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// The task that completes when public Task StopAndWaitAsync() { + Check(); + _cts?.Cancel(); return WaitForAllTask; } @@ -103,7 +110,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack private void OnAllServerExit(Task allExit) { //Unload the hosts - _serviceDomain.UnloadAll(); + _serviceDomain.TearDown(); } /// diff --git a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs index bb6e96f..0b75031 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -22,6 +22,10 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; +using System.Linq; +using System.Collections.Generic; + using VNLib.Net.Http; namespace VNLib.Plugins.Essentials.ServiceStack diff --git a/lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs b/lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs index 0871fdc..fb9c340 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs @@ -1,12 +1,12 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack -* File: IPluginController.cs +* File: IPluginManager.cs * -* IPluginController.cs is part of VNLib.Plugins.Essentials.ServiceStack which is part of the larger -* VNLib collection of libraries and utilities. +* IPluginManager.cs is part of VNLib.Plugins.Essentials.ServiceStack which +* is part of the larger VNLib collection of libraries and utilities. * * VNLib.Plugins.Essentials.ServiceStack is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -22,7 +22,9 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System.Text.Json; +using System; +using System.Threading.Tasks; +using System.Collections.Generic; using VNLib.Utils.Logging; @@ -32,8 +34,13 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// Represents a live plugin controller that manages all /// plugins loaded in a /// - public interface IPluginController + public interface IPluginManager { + /// + /// The the plugins managed by this + /// + public IEnumerable Plugins { get; } + /// /// Loads all plugins specified by the host config to the service manager, /// or attempts to load plugins by the default @@ -41,7 +48,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// The configuration instance to pass to plugins /// A log provider to write message and errors to /// A task that resolves when all plugins are loaded - Task LoadPlugins(JsonDocument config, ILogProvider appLog); + Task LoadPluginsAsync(PluginLoadConfiguration config, ILogProvider appLog); /// /// Sends a message to a plugin identified by it's name. @@ -61,11 +68,8 @@ namespace VNLib.Plugins.Essentials.ServiceStack void ForceReloadAllPlugins(); /// - /// Unloads all service groups, removes them, and unloads all - /// loaded plugins + /// Unloads all loaded plugins and calls thier event handlers /// - /// - /// - void UnloadAll(); + void UnloadPlugins(); } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/IPluginWrapper.cs b/lib/Plugins.Essentials.ServiceStack/src/IPluginWrapper.cs new file mode 100644 index 0000000..2e686ee --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/IPluginWrapper.cs @@ -0,0 +1,54 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: IPluginWrapper.cs +* +* IPluginWrapper.cs is part of VNLib.Plugins.Essentials.ServiceStack which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.ServiceStack 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 2 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.ServiceStack 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 VNLib.Plugins.Runtime; + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + + /// + /// Represents a plugin managed by a that includes dynamically loaded plugins + /// + public interface IManagedPlugin + { + /// + /// Exposes the internal for the loaded plugin + /// + PluginController Controller { get; } + + /// + /// The file path to the loaded plugin + /// + string PluginPath { get; } + + /// + /// The exposed services the inernal plugin provides + /// + /// + /// WARNING: Services exposed by the plugin will abide by the plugin lifecycle, so consumers + /// must listen for plugin load/unload events to respect lifecycles properly. + /// + IUnloadableServiceProvider Services { get; } + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs b/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs index 0c8d6c1..bb4f65f 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -22,21 +22,43 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using VNLib.Net.Http; + namespace VNLib.Plugins.Essentials.ServiceStack { /// - /// Represents a host that exposes a processor for host events + /// Represents an HTTP service host which provides information required + /// for HttpServer routing and the for proccessing + /// incomming connections /// public interface IServiceHost { /// - /// The to process - /// incoming HTTP connections + /// The that handles HTTP connection + /// processing. /// - EventProcessor Processor { get; } + IWebRoot Processor { get; } + /// /// The host's transport infomration /// IHostTransportInfo TransportInfo { get; } + + /// + /// Called when a plugin is loaded and is endpoints are extracted + /// to be placed into service. + /// + /// The loaded plugin ready to be attached + /// The dynamic endpoints of a loading plugin + void OnRuntimeServiceAttach(IManagedPlugin plugin, IEndpoint[] endpoints); + + /// + /// Called when a 's + /// unloads a given plugin, and its originally discovered endpoints + /// + /// The unloading plugin to detach + /// The endpoints of the unloading plugin to remove from service + void OnRuntimeServiceDetach(IManagedPlugin plugin, IEndpoint[] endpoints); + } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/IUnloadableServiceProvider.cs b/lib/Plugins.Essentials.ServiceStack/src/IUnloadableServiceProvider.cs new file mode 100644 index 0000000..fa334bd --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/IUnloadableServiceProvider.cs @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: IUnloadableServiceProvider.cs +* +* IUnloadableServiceProvider.cs is part of VNLib.Plugins.Essentials.ServiceStack +* which is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.ServiceStack 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 2 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.ServiceStack 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; +using System.Threading; + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + /// + /// A that may be unloaded when the + /// assembly that is sharing the types are being disposed. + /// + public interface IUnloadableServiceProvider : IServiceProvider + { + /// + /// A token that is set cancelled state when the service provider + /// is unloaded. + /// + CancellationToken UnloadToken { get; } + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs b/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs new file mode 100644 index 0000000..596ea83 --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs @@ -0,0 +1,202 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: ManagedPlugin.cs +* +* ManagedPlugin.cs is part of VNLib.Plugins.Essentials.ServiceStack which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.ServiceStack 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 2 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.ServiceStack 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; +using System.IO; +using System.Linq; +using System.Threading; +using System.Reflection; +using System.Threading.Tasks; +using System.ComponentModel.Design; + +using VNLib.Utils; +using VNLib.Plugins.Runtime; +using VNLib.Plugins.Attributes; + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + + internal sealed class ManagedPlugin : VnDisposeable, IPluginEventListener, IManagedPlugin + { + private readonly IPluginEventListener _serviceDomainListener; + private readonly RuntimePluginLoader _plugin; + + private UnloadableServiceContainer? _services; + + public ManagedPlugin(string pluginPath, PluginLoadConfiguration config, IPluginEventListener listener) + { + PluginPath = pluginPath; + + //configure the loader + _plugin = new(pluginPath, config.HostConfig, config.PluginErrorLog, config.HotReload, config.HotReload); + + //Register listener before loading occurs + _plugin.Controller.Register(this, this); + + //Store listener to raise events + _serviceDomainListener = listener; + } + + /// + public string PluginPath { get; } + + /// + public IUnloadableServiceProvider Services + { + get + { + Check(); + return _services!; + } + } + + /// + public PluginController Controller + { + get + { + Check(); + return _plugin.Controller; + } + } + + internal string PluginFileName => Path.GetFileName(PluginPath); + + internal Task InitializePluginsAsync() + { + Check(); + return _plugin.InitializeController(); + } + + internal void LoadPlugins() + { + Check(); + _plugin.LoadPlugins(); + } + + /* + * Automatically called after the plugin has successfully loaded + * by event handlers below + */ + private void ConfigureServices() + { + //If the service container is defined, dispose + _services?.Dispose(); + + //Init new service container + _services = new(); + + //Get types from plugin + foreach (LivePlugin plugin in _plugin.Controller.Plugins) + { + /* + * Get the exposed configurator method if declared, + * it may not be defined. + */ + ServiceConfigurator? callback = plugin.PluginType.GetMethods() + .Where(static m => m.GetCustomAttribute() != null && !m.IsAbstract) + .Select(m => m.CreateDelegate(plugin.Plugin)) + .FirstOrDefault(); + + //Invoke if defined to expose services + callback?.Invoke(_services); + } + } + + internal void ReloadPlugins() + { + Check(); + _plugin.ReloadPlugins(); + } + + internal void UnloadPlugins() + { + Check(); + + //unload plugins + _plugin.UnloadAll(); + + //Services will be cleaned up by the unload event + } + + void IPluginEventListener.OnPluginLoaded(PluginController controller, object? state) + { + //Initialize services after load, before passing event + ConfigureServices(); + + //Propagate event + _serviceDomainListener.OnPluginLoaded(controller, state); + } + + void IPluginEventListener.OnPluginUnloaded(PluginController controller, object? state) + { + //Cleanup services no longer in use. Plugin is still valid until this method returns + using (_services) + { + //Propagate event + _serviceDomainListener.OnPluginUnloaded(controller, state); + + //signal service cancel before disposing + _services?.SignalUnload(); + } + //Remove ref to services + _services = null; + } + + protected override void Free() + { + //Dispose services + _services?.Dispose(); + //Unregister the listener to cleanup resources + _plugin.Controller.Unregister(this); + //Dispose loader + _plugin.Dispose(); + } + + + private sealed class UnloadableServiceContainer : ServiceContainer, IUnloadableServiceProvider + { + private readonly CancellationTokenSource _cts; + + public UnloadableServiceContainer() : base() + { + _cts = new(); + } + + /// + CancellationToken IUnloadableServiceProvider.UnloadToken => _cts.Token; + + /// + /// Signals to listensers that the service container will be unloading + /// + internal void SignalUnload() => _cts.Cancel(); + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _cts.Dispose(); + } + } + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs b/lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs new file mode 100644 index 0000000..4974e71 --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs @@ -0,0 +1,62 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: PluginLoadConfiguration.cs +* +* PluginLoadConfiguration.cs is part of VNLib.Plugins.Essentials.ServiceStack +* which is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.ServiceStack 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 2 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.ServiceStack 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.Text.Json; + +using VNLib.Utils.Logging; +using VNLib.Plugins.Runtime; + + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + /// + /// Plugin loading configuration variables + /// + public readonly record struct PluginLoadConfiguration + { + /// + /// The directory containing the dynamic plugin assemblies to load + /// + public readonly string PluginDir { get; init; } + + /// + /// A value that indicates if the internal + /// allows for hot-reload/unloadable plugin assemblies. + /// + public readonly bool HotReload { get; init; } + + /// + /// The optional host configuration file to merge with plugin config + /// to pass to the loading plugin. + /// + public readonly JsonDocument? HostConfig { get; init; } + + /// + /// Passed to the underlying + /// holding plugins + /// + public readonly ILogProvider? PluginErrorLog { get; init; } + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs b/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs new file mode 100644 index 0000000..cdcf7ba --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs @@ -0,0 +1,240 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: PluginManager.cs +* +* PluginManager.cs is part of VNLib.Plugins.Essentials.ServiceStack which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.ServiceStack 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 2 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.ServiceStack 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; +using System.IO; +using System.Linq; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Utils; +using VNLib.Utils.IO; +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; +using VNLib.Plugins.Runtime; + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + + /// + /// A sealed type that manages the plugin interaction layer. Manages the lifetime of plugin + /// instances, exposes controls, and relays stateful plugin events. + /// + internal sealed class PluginManager : VnDisposeable, IPluginManager, IPluginEventListener + { + private const string PLUGIN_FILE_EXTENSION = ".dll"; + + private readonly List _plugins; + private readonly IReadOnlyCollection _dependents; + + + private IEnumerable _livePlugins => _plugins.SelectMany(static p => p.Controller.Plugins); + + /// + /// The collection of internal controllers + /// + public IEnumerable Plugins => _plugins; + + public PluginManager(IReadOnlyCollection dependents) + { + _plugins = new(); + _dependents = dependents; + } + + /// + /// + public Task LoadPluginsAsync(PluginLoadConfiguration config, ILogProvider appLog) + { + Check(); + + //Load all virtual file assemblies withing the plugin folder + DirectoryInfo dir = new(config.PluginDir); + + if (!dir.Exists) + { + appLog.Warn("Plugin directory {dir} does not exist. No plugins were loaded", config.PluginDir); + return Task.CompletedTask; + } + + appLog.Information("Loading plugins. Hot-reload: {en}", config.HotReload); + + //Enumerate all dll files within this dir + IEnumerable dirs = dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly); + + //Select only dirs with a dll that is named after the directory name + IEnumerable pluginPaths = GetPluginPaths(dirs); + + IEnumerable pluginFileNames = pluginPaths.Select(static s => $"{Path.GetFileName(s)}\n"); + + appLog.Debug("Found plugin files: \n{files}", string.Concat(pluginFileNames)); + + //Initialze plugin managers + ManagedPlugin[] wrappers = pluginPaths.Select(pw => new ManagedPlugin(pw, config, this)).ToArray(); + + //Add to loaded plugins + _plugins.AddRange(wrappers); + + //Load plugins + return InitiailzeAndLoadAsync(appLog); + } + + private static IEnumerable GetPluginPaths(IEnumerable dirs) + { + //Select only dirs with a dll that is named after the directory name + return dirs.Where(static pdir => + { + string compined = Path.Combine(pdir.FullName, pdir.Name); + string FilePath = string.Concat(compined, PLUGIN_FILE_EXTENSION); + return FileOperations.FileExists(FilePath); + }) + //Return the name of the dll file to import + .Select(static pdir => + { + string compined = Path.Combine(pdir.FullName, pdir.Name); + return string.Concat(compined, PLUGIN_FILE_EXTENSION); + }); + } + + private async Task InitiailzeAndLoadAsync(ILogProvider debugLog) + { + //Load all async + Task[] initAll = _plugins.Select(p => InitializePlugin(p, debugLog)).ToArray(); + + //Wait for initalization + await Task.WhenAll(initAll).ConfigureAwait(false); + + //Load stage, load all multithreaded + Parallel.ForEach(_plugins, p => LoadPlugin(p, debugLog)); + + debugLog.Information("Plugin loading completed"); + } + + private async Task InitializePlugin(ManagedPlugin plugin, ILogProvider debugLog) + { + try + { + //Load wrapper + await plugin.InitializePluginsAsync().ConfigureAwait(true); + } + catch (Exception ex) + { + debugLog.Error(ex, $"Exception raised during initialzation of {plugin.PluginFileName}. It has been removed from the collection\n{ex}"); + + //Remove the plugin from the list while locking it + lock (_plugins) + { + _plugins.Remove(plugin); + } + + //Dispose the plugin + plugin.Dispose(); + } + } + + private static void LoadPlugin(ManagedPlugin plugin, ILogProvider debugLog) + { + Stopwatch sw = new(); + try + { + sw.Start(); + + //Load wrapper + plugin.LoadPlugins(); + + sw.Stop(); + + debugLog.Verbose("Loaded {pl} in {tm} ms", plugin.PluginFileName, sw.ElapsedMilliseconds); + } + catch (Exception ex) + { + debugLog.Error(ex, $"Exception raised during loading {plugin.PluginFileName}. Failed to load plugin \n{ex}"); + } + finally + { + sw.Stop(); + } + } + + /// + public bool SendCommandToPlugin(string pluginName, string message, StringComparison nameComparison = StringComparison.Ordinal) + { + Check(); + + //Find the single plugin by its name + LivePlugin? pl = _livePlugins.Where(p => pluginName.Equals(p.PluginName, nameComparison)).SingleOrDefault(); + + //Send the command + return pl?.SendConsoleMessage(message) ?? false; + } + + /// + public void ForceReloadAllPlugins() + { + //Reload all plugin managers + _plugins.TryForeach(static p => p.ReloadPlugins()); + } + + /// + public void UnloadPlugins() + { + //Unload all plugin controllers + _plugins.TryForeach(static p => p.UnloadPlugins()); + + /* + * All plugin instances must be destroyed because the + * only way they will be loaded is from their files + * again, so they must be released + */ + _plugins.TryForeach(static p => p.Dispose()); + _plugins.Clear(); + } + + protected override void Free() + { + //Cleanup on dispose if unload failed + _plugins.TryForeach(static p => p.Dispose()); + _plugins.Clear(); + } + + void IPluginEventListener.OnPluginLoaded(PluginController controller, object? state) + { + //Get event listeners at event time because deps may be modified by the domain + ServiceGroup[] deps = _dependents.Select(static d => d).ToArray(); + + //run onload method + deps.TryForeach(d => d.OnPluginLoaded((IManagedPlugin)state!)); + } + + void IPluginEventListener.OnPluginUnloaded(PluginController controller, object? state) + { + //Get event listeners at event time because deps may be modified by the domain + ServiceGroup[] deps = _dependents.Select(static d => d).ToArray(); + + //Run unloaded method + deps.TryForeach(d => d.OnPluginUnloaded((IManagedPlugin)state!)); + } + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs b/lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs index 7b06e70..f0f9559 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -24,52 +24,43 @@ using System; using System.Net; -using System.Text.Json; -using System.Diagnostics; +using System.Linq; +using System.Collections.Generic; using VNLib.Utils; -using VNLib.Utils.IO; using VNLib.Utils.Extensions; -using VNLib.Utils.Logging; -using VNLib.Plugins.Runtime; -using VNLib.Plugins.Essentials.Content; -using VNLib.Plugins.Essentials.Sessions; namespace VNLib.Plugins.Essentials.ServiceStack { + /// /// Represents a domain of services and thier dynamically loaded plugins /// that will be hosted by an application service stack /// - public sealed class ServiceDomain : VnDisposeable, IPluginController + public sealed class ServiceDomain : VnDisposeable { - private const string PLUGIN_FILE_EXTENSION = ".dll"; - private const string DEFUALT_PLUGIN_DIR = "/plugins"; - private const string PLUGINS_CONFIG_ELEMENT = "plugins"; - private readonly LinkedList _serviceGroups; - private readonly LinkedList _pluginLoaders; - - /// - /// Enumerates all loaded plugin instances - /// - public IEnumerable Plugins => _pluginLoaders.SelectMany(static s => - s.LivePlugins.Where(static p => p.Plugin != null) - .Select(static s => s.Plugin!) - ); + private readonly PluginManager _plugins; /// /// Gets all service groups loaded in the service manager /// public IReadOnlyCollection ServiceGroups => _serviceGroups; + /// + /// Gets the internal that manages plugins for the entire + /// + /// + public IPluginManager PluginManager => _plugins; + /// /// Initializes a new empty /// public ServiceDomain() { _serviceGroups = new(); - _pluginLoaders = new(); + //Init plugin manager and pass ref to service group collection + _plugins = new PluginManager(_serviceGroups); } /// @@ -78,8 +69,11 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// /// The callback method to build virtual hosts /// A value that indicates if any virtual hosts were successfully loaded + /// public bool BuildDomain(Action> hostBuilder) { + Check(); + //LL to store created hosts LinkedList hosts = new(); @@ -94,8 +88,11 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// /// The enumeration of virtual hosts /// A value that indicates if any virtual hosts were successfully loaded + /// public bool FromExisting(IEnumerable hosts) { + Check(); + //Get service groups and pass service group list CreateServiceGroups(_serviceGroups, hosts); return _serviceGroups.Any(); @@ -111,11 +108,12 @@ namespace VNLib.Plugins.Essentials.ServiceStack { IEnumerable groupHosts = hosts.Where(host => host.TransportInfo.TransportEndpoint.Equals(iface)); - IServiceHost[]? overlap = groupHosts.Where(vh => groupHosts.Select(static s => s.Processor.Hostname).Count(hostname => vh.Processor.Hostname == hostname) > 1).ToArray(); + //Find any duplicate hostnames for the same service gorup + IServiceHost[] overlap = groupHosts.Where(vh => groupHosts.Select(static s => s.Processor.Hostname).Count(hostname => vh.Processor.Hostname == hostname) > 1).ToArray(); - foreach (IServiceHost vh in overlap) + if(overlap.Length > 0) { - throw new ArgumentException($"The hostname '{vh.Processor.Hostname}' is already in use by another virtual host"); + throw new ArgumentException($"The hostname '{overlap.Last().Processor.Hostname}' is already in use by another virtual host"); } //init new service group around an interface and its roots @@ -125,235 +123,33 @@ namespace VNLib.Plugins.Essentials.ServiceStack } } - /// - public Task LoadPlugins(JsonDocument config, ILogProvider appLog) - { - if (!config.RootElement.TryGetProperty(PLUGINS_CONFIG_ELEMENT, out JsonElement pluginEl)) - { - appLog.Information("Plugins element not defined in config, skipping plugin loading"); - return Task.CompletedTask; - } - - //Get the plugin directory, or set to default - string pluginDir = pluginEl.GetPropString("path") ?? Path.Combine(Directory.GetCurrentDirectory(), DEFUALT_PLUGIN_DIR); - //Get the hot reload flag - bool hotReload = pluginEl.TryGetProperty("hot_reload", out JsonElement hrel) && hrel.GetBoolean(); - - //Load all virtual file assemblies withing the plugin folder - DirectoryInfo dir = new(pluginDir); - - if (!dir.Exists) - { - appLog.Warn("Plugin directory {dir} does not exist. No plugins were loaded", pluginDir); - return Task.CompletedTask; - } - - appLog.Information("Loading plugins. Hot-reload: {en}", hotReload); - - //Enumerate all dll files within this dir - IEnumerable dirs = dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly); - - //Select only dirs with a dll that is named after the directory name - IEnumerable pluginPaths = dirs.Where(static pdir => - { - string compined = Path.Combine(pdir.FullName, pdir.Name); - string FilePath = string.Concat(compined, PLUGIN_FILE_EXTENSION); - return FileOperations.FileExists(FilePath); - }) - //Return the name of the dll file to import - .Select(static pdir => - { - string compined = Path.Combine(pdir.FullName, pdir.Name); - return string.Concat(compined, PLUGIN_FILE_EXTENSION); - }); - - IEnumerable pluginFileNames = pluginPaths.Select(static s => $"{Path.GetFileName(s)}\n"); - - appLog.Debug("Found plugin files: \n{files}", string.Concat(pluginFileNames)); - - LinkedList loading = new(); - - object listLock = new(); - - foreach (string pluginPath in pluginPaths) - { - async Task Load() - { - string pluginName = Path.GetFileName(pluginPath); - - RuntimePluginLoader plugin = new(pluginPath, config, appLog, hotReload, hotReload); - Stopwatch sw = new(); - try - { - sw.Start(); - - await plugin.InitLoaderAsync(); - - //Listen for reload events to remove and re-add endpoints - plugin.Reloaded += OnPluginReloaded; - - lock (listLock) - { - //Add to list - _pluginLoaders.AddLast(plugin); - } - - sw.Stop(); - - appLog.Verbose("Loaded {pl} in {tm} ms", pluginName, sw.ElapsedMilliseconds); - } - catch (Exception ex) - { - appLog.Error(ex, $"Exception raised during loading {pluginName}. Failed to load plugin \n{ex}"); - plugin.Dispose(); - } - finally - { - sw.Stop(); - } - } - - loading.AddLast(Load()); - } - - //Continuation to add all initial plugins to the service manager - void Continuation(Task t) - { - appLog.Verbose("Plugins loaded"); - - //Add inital endpoints for all plugins - _pluginLoaders.TryForeach(ldr => _serviceGroups.TryForeach(sg => sg.AddOrUpdateEndpointsForPlugin(ldr))); - - //Init session provider - InitSessionProvider(); - - //Init page router - InitPageRouter(); - } - - //wait for loading to completed - return Task.WhenAll(loading.ToArray()).ContinueWith(Continuation, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } - - /// - public bool SendCommandToPlugin(string pluginName, string message, StringComparison nameComparison = StringComparison.Ordinal) - { - Check(); - //Find the single plugin by its name - LivePlugin? pl = _pluginLoaders.Select(p => - p.LivePlugins.Where(lp => pluginName.Equals(lp.PluginName, nameComparison)) - ) - .SelectMany(static lp => lp) - .SingleOrDefault(); - //Send the command - return pl?.SendConsoleMessage(message) ?? false; - } - - /// - public void ForceReloadAllPlugins() - { - Check(); - _pluginLoaders.TryForeach(static pl => pl.ReloadPlugin()); - } - - /// - public void UnloadAll() + /// + /// Tears down the service domain by unloading all plugins (calling their event handlers) + /// and destroying all s. This instance may be rebuilt if this + /// method returns successfully. + /// + internal void TearDown() { Check(); - //Unload service groups before unloading plugins + /* + * Unloading plugins should trigger the OnPluginUnloading + * hook which should cause all dependencies to unload linked + * types. + */ + _plugins.UnloadPlugins(); + + //Manually cleanup if unload missed data _serviceGroups.TryForeach(static sg => sg.UnloadAll()); //empty service groups _serviceGroups.Clear(); - - //Unload all plugins - _pluginLoaders.TryForeach(static pl => pl.UnloadAll()); - } - - private void OnPluginReloaded(object? plugin, EventArgs empty) - { - //Update endpoints for the loader - RuntimePluginLoader reloaded = (plugin as RuntimePluginLoader)!; - - //Update all endpoints for the plugin - _serviceGroups.TryForeach(sg => sg.AddOrUpdateEndpointsForPlugin(reloaded)); - } - - private void InitSessionProvider() - { - //Callback to reload provider - void onSessionProviderReloaded(ISessionProvider old, ISessionProvider current) - { - _serviceGroups.TryForeach(sg => sg.UpdateSessionProvider(current)); - } - - try - { - //get the loader that contains the single session provider - RuntimePluginLoader? sessionLoader = _pluginLoaders - .Where(static s => s.ExposesType()) - .SingleOrDefault(); - - //If session provider has been supplied, load it - if (sessionLoader != null) - { - //Get the session provider from the plugin loader - ISessionProvider sp = sessionLoader.GetExposedTypeFromPlugin()!; - - //Init inital provider - onSessionProviderReloaded(null!, sp); - - //Register reload event - sessionLoader.RegisterListenerForSingle(onSessionProviderReloaded); - } - } - catch (InvalidOperationException) - { - throw new TypeLoadException("More than one session provider plugin was defined in the plugin directory, cannot continue"); - } - } - - private void InitPageRouter() - { - //Callback to reload provider - void onRouterReloaded(IPageRouter old, IPageRouter current) - { - _serviceGroups.TryForeach(sg => sg.UpdatePageRouter(current)); - } - - try - { - - //get the loader that contains the single page router - RuntimePluginLoader? routerLoader = _pluginLoaders - .Where(static s => s.ExposesType()) - .SingleOrDefault(); - - //If router has been supplied, load it - if (routerLoader != null) - { - //Get initial value - IPageRouter sp = routerLoader.GetExposedTypeFromPlugin()!; - - //Init inital provider - onRouterReloaded(null!, sp); - - //Register reload event - routerLoader.RegisterListenerForSingle(onRouterReloaded); - } - } - catch (InvalidOperationException) - { - throw new TypeLoadException("More than one page router plugin was defined in the plugin directory, cannot continue"); - } } + /// protected override void Free() { - //Dispose loaders - _pluginLoaders.TryForeach(static pl => pl.Dispose()); - _pluginLoaders.Clear(); + _plugins.Dispose(); _serviceGroups.Clear(); } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs b/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs index f57a6f9..2801776 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -23,13 +23,11 @@ */ using System.Net; +using System.Linq; using System.Collections.Generic; using System.Runtime.CompilerServices; using VNLib.Utils.Extensions; -using VNLib.Plugins.Runtime; -using VNLib.Plugins.Essentials.Content; -using VNLib.Plugins.Essentials.Sessions; namespace VNLib.Plugins.Essentials.ServiceStack { @@ -42,7 +40,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack public sealed class ServiceGroup { private readonly LinkedList _vHosts; - private readonly ConditionalWeakTable _endpointsForPlugins; + private readonly ConditionalWeakTable _endpointsForPlugins; /// /// The transport endpoint for all loaded service hosts @@ -68,61 +66,43 @@ namespace VNLib.Plugins.Essentials.ServiceStack } /// - /// Sets the specified page rotuer for all virtual hosts + /// Manually detatches runtime services and their loaded endpoints from all + /// endpoints. /// - /// The page router to user - internal void UpdatePageRouter(IPageRouter router) => _vHosts.TryForeach(v => v.Processor.SetPageRouter(router)); - /// - /// Sets the specified session provider for all virtual hosts - /// - /// The session provider to use - internal void UpdateSessionProvider(ISessionProvider current) => _vHosts.TryForeach(v => v.Processor.SetSessionProvider(current)); + internal void UnloadAll() + { + //Remove all loaded endpoints + _vHosts.TryForeach(v => _endpointsForPlugins.TryForeach(eps => v.OnRuntimeServiceDetach(eps.Key, eps.Value))); - /// - /// Adds or updates all endpoints exported by all plugins - /// within the specified loader. All endpoints exposed - /// by a previously loaded instance are removed and all - /// currently exposed endpoints are added to all virtual - /// hosts - /// - /// The plugin loader to get add/update endpoints from - internal void AddOrUpdateEndpointsForPlugin(RuntimePluginLoader loader) + //Clear all hosts + _vHosts.Clear(); + //Clear all endpoints + _endpointsForPlugins.Clear(); + } + + internal void OnPluginLoaded(IManagedPlugin controller) { //Get all new endpoints for plugin - IEndpoint[] newEndpoints = loader.LivePlugins.SelectMany(static pl => pl.Plugin!.GetEndpoints()).ToArray(); - - //See if - if(_endpointsForPlugins.TryGetValue(loader, out IEndpoint[]? oldEps)) - { - //Remove old endpoints - _vHosts.TryForeach(v => v.Processor.RemoveEndpoint(oldEps)); - } + IEndpoint[] newEndpoints = controller.Controller.Plugins.SelectMany(static pl => pl.Plugin!.GetEndpoints()).ToArray(); //Add endpoints to dict - _endpointsForPlugins.AddOrUpdate(loader, newEndpoints); + _endpointsForPlugins.AddOrUpdate(controller, newEndpoints); //Add endpoints to hosts - _vHosts.TryForeach(v => v.Processor.AddEndpoint(newEndpoints)); + _vHosts.TryForeach(v => v.OnRuntimeServiceAttach(controller, newEndpoints)); } - /// - /// Unloads all previously stored endpoints, router, session provider, and - /// clears all internal data structures - /// - internal void UnloadAll() + internal void OnPluginUnloaded(IManagedPlugin controller) { - //Remove all loaded endpoints - _vHosts.TryForeach(v => _endpointsForPlugins.TryForeach(eps => v.Processor.RemoveEndpoint(eps.Value))); - - //Remove all routers - _vHosts.TryForeach(static v => v.Processor.SetPageRouter(null)); - //Remove all session providers - _vHosts.TryForeach(static v => v.Processor.SetSessionProvider(null)); + //Get the old endpoints from the controller referrence and remove them + if (_endpointsForPlugins.TryGetValue(controller, out IEndpoint[]? oldEps)) + { + //Remove the old endpoints + _vHosts.TryForeach(v => v.OnRuntimeServiceDetach(controller, oldEps)); - //Clear all hosts - _vHosts.Clear(); - //Clear all endpoints - _endpointsForPlugins.Clear(); + //remove controller ref + _ = _endpointsForPlugins.Remove(controller); + } } } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/VNLib.Plugins.Essentials.ServiceStack.csproj b/lib/Plugins.Essentials.ServiceStack/src/VNLib.Plugins.Essentials.ServiceStack.csproj index 4918c49..dd7b562 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/VNLib.Plugins.Essentials.ServiceStack.csproj +++ b/lib/Plugins.Essentials.ServiceStack/src/VNLib.Plugins.Essentials.ServiceStack.csproj @@ -2,7 +2,6 @@ net6.0 - enable VNLib.Plugins.Essentials.ServiceStack VNLib.Plugins.Essentials.ServiceStack enable diff --git a/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs b/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs index 610d646..75c3388 100644 --- a/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs +++ b/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs @@ -1,11 +1,11 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials -* File: AccountManager.cs +* File: AccountUtil.cs * -* AccountManager.cs is part of VNLib.Plugins.Essentials which is part of the larger +* AccountUtil.cs is part of VNLib.Plugins.Essentials which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify @@ -23,21 +23,19 @@ */ using System; -using System.IO; -using System.Text; +using System.Buffers; +using System.Threading; using System.Threading.Tasks; using System.Security.Cryptography; using System.Text.RegularExpressions; using System.Runtime.CompilerServices; using VNLib.Hashing; -using VNLib.Net.Http; using VNLib.Utils; using VNLib.Utils.Memory; using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Users; using VNLib.Plugins.Essentials.Sessions; -using VNLib.Plugins.Essentials.Extensions; #nullable enable @@ -51,60 +49,24 @@ namespace VNLib.Plugins.Essentials.Accounts /// public static partial class AccountUtil { - public const int MAX_EMAIL_CHARS = 50; - public const int ID_FIELD_CHARS = 65; - public const int STREET_ADDR_CHARS = 150; - public const int MAX_LOGIN_COUNT = 10; - public const int MAX_FAILED_RESET_ATTEMPS = 5; - /// - /// The maximum time in seconds for a login message to be considered valid - /// - public const double MAX_TIME_DIFF_SECS = 10.00; /// /// The size in bytes of the random passwords generated when invoking the /// public const int RANDOM_PASS_SIZE = 128; - /// - /// The name of the header that will identify a client's identiy - /// - public const string LOGIN_TOKEN_HEADER = "X-Web-Token"; + /// /// The origin string of a local user account. This value will be set if an /// account is created through the VNLib.Plugins.Essentials.Accounts library /// public const string LOCAL_ACCOUNT_ORIGIN = "local"; - /// - /// The size (in bytes) of the challenge secret - /// - public const int CHALLENGE_SIZE = 64; - /// - /// The size (in bytes) of the sesssion long user-password challenge - /// - public const int SESSION_CHALLENGE_SIZE = 128; - - //The buffer size to use when decoding the base64 public key from the user - private const int PUBLIC_KEY_BUFFER_SIZE = 1024; - /// - /// The name of the login cookie set when a user logs in - /// - public const string LOGIN_COOKIE_NAME = "VNLogin"; - /// - /// The name of the login client identifier cookie (cookie that is set fir client to use to determine if the user is logged in) - /// - public const string LOGIN_COOKIE_IDENTIFIER = "li"; - - private const int LOGIN_COOKIE_SIZE = 64; - + + //Session entry keys private const string BROWSER_ID_ENTRY = "acnt.bid"; - private const string CLIENT_PUB_KEY_ENTRY = "acnt.pbk"; - private const string CHALLENGE_HMAC_ENTRY = "acnt.cdig"; private const string FAILED_LOGIN_ENTRY = "acnt.flc"; private const string LOCAL_ACCOUNT_ENTRY = "acnt.ila"; private const string ACC_ORIGIN_ENTRY = "__.org"; - private const string TOKEN_UPDATE_TIME_ENTRY = "acnt.tut"; - //private const string CHALLENGE_HASH_ENTRY = "acnt.chl"; //Privlage masks public const ulong READ_MSK = 0x0000000000000001L; @@ -122,19 +84,6 @@ namespace VNLib.Plugins.Essentials.Accounts public const ulong MINIMUM_LEVEL = 0x0000000100000001L; - //Timeouts - public static readonly TimeSpan LoginCookieLifespan = TimeSpan.FromHours(1); - public static readonly TimeSpan RegenIdPeriod = TimeSpan.FromMinutes(25); - - /// - /// The client data encryption padding. - /// - public static readonly RSAEncryptionPadding ClientEncryptonPadding = RSAEncryptionPadding.OaepSHA256; - - /// - /// The size (in bytes) of the web-token hash size - /// - private static readonly int TokenHashSize = (SHA384.Create().HashSize / 8); /// /// Speical character regual expresion for basic checks @@ -154,7 +103,7 @@ namespace VNLib.Plugins.Essentials.Accounts /// /// /// - public static async Task SetRandomPasswordAsync(this PasswordHashing passHashing, IUserManager manager, IUser user, int size = RANDOM_PASS_SIZE) + public static async Task SetRandomPasswordAsync(this IPasswordHashingProvider passHashing, IUserManager manager, IUser user, int size = RANDOM_PASS_SIZE) { _ = manager ?? throw new ArgumentNullException(nameof(manager)); _ = user ?? throw new ArgumentNullException(nameof(user)); @@ -208,512 +157,292 @@ namespace VNLib.Plugins.Essentials.Accounts [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetRandomUserId() => RandomHash.GetRandomHash(HashAlg.SHA1, 64, HashEncodingMode.Hexadecimal); - #endregion - - #region Client Auth Extensions - /// - /// Runs necessary operations to grant authorization to the specified user of a given session and user with provided variables + /// Generates a cryptographically secure random password, then hashes it + /// and returns the hash of the new password /// - /// The connection and session to log-in - /// The message of the client to set the log-in status of - /// The user to log-in - /// The encrypted base64 token secret data to send to the client - /// - /// - public static string GenerateAuthorization(this HttpEntity ev, LoginMessage loginMessage, IUser user) + /// + /// The size (in bytes) of the new random password + /// A that contains the new password hash + public static PrivateString GetRandomPassword(this IPasswordHashingProvider hashing, int size = RANDOM_PASS_SIZE) { - return GenerateAuthorization(ev, loginMessage.ClientPublicKey, loginMessage.ClientID, user); - } + //Get random bytes + byte[] randBuffer = ArrayPool.Shared.Rent(size); + try + { + Span span = randBuffer.AsSpan(0, size); - /// - /// Runs necessary operations to grant authorization to the specified user of a given session and user with provided variables - /// - /// The connection and session to log-in - /// The clients base64 public key - /// The browser/client id - /// The user to log-in - /// The encrypted base64 token secret data to send to the client - /// - /// - /// - public static string GenerateAuthorization(this HttpEntity ev, string base64PubKey, string clientId, IUser user) - { - if (!ev.Session.IsSet || ev.Session.SessionType != SessionType.Web) + //Generate random password + RandomHash.GetRandomBytes(span); + + //hash the password + return hashing.Hash(span); + } + finally { - throw new InvalidOperationException("The session is not set or the session is not a web-based session type"); + //Zero the block and return to pool + MemoryUtil.InitializeBlock(randBuffer.AsSpan()); + ArrayPool.Shared.Return(randBuffer); } - //Update session-id for "upgrade" - ev.Session.RegenID(); - //derrive token from login data - TryGenerateToken(base64PubKey, out string base64ServerToken, out string base64ClientData); - //Clear flags - user.FailedLoginCount(0); - //Get the "local" account flag from the user object - bool localAccount = user.IsLocalAccount(); - //Set login cookie and session login hash - ev.SetLogin(localAccount); - //Store variables - ev.Session.UserID = user.UserID; - ev.Session.Privilages = user.Privilages; - //Store browserid/client id if specified - SetBrowserID(in ev.Session, clientId); - //Store the clients public key - SetBrowserPubKey(in ev.Session, base64PubKey); - //Set local account flag - ev.Session.HasLocalAccount(localAccount); - //Store the base64 server key to compute the hmac later - ev.Session.Token = base64ServerToken; - //Update the last token upgrade time - ev.Session.LastTokenUpgrade(ev.RequestedTimeUtc); - //Return the client encrypted data - return base64ClientData; } - /* - * Notes for RSA client token generator code below - * - * To log-in a client with the following API the calling code - * must have already determined that the client should be - * logged in (verified passwords or auth tokens). - * - * The client will send a LoginMessage object that will - * contain the following Information. - * - * - The clients RSA public key in base64 subject-key info format - * - The client browser's id hex string - * - The clients local-time - * - * The TryGenerateToken method, will generate a random-byte token, - * encrypt it using the clients RSA public key, return the encrypted - * token data to the client, and only the client will be able to - * decrypt the token data. - * - * The token data is also hashed with SHA-256 (for future use) and - * stored in the client's session store. The client must decrypt - * the token data, hash it, and return it as a header for verification. - * - * Ideally the client should sign the data and send the signature or - * hash back, but it wont prevent MITM, and for now I think it just - * adds extra overhead for every connection during the HttpEvent.TokenMatches() - * check extension method - */ - - private ref struct TokenGenBuffers + /// + /// Asynchronously verifies the desired user's password. If the user is not found or the password is not found + /// returns false. Returns true if the user exist's has a valid password hash and matches the supplied password value. + /// + /// + /// The id of the user to check the password against + /// The raw password of the user to compare hashes against + /// The password hashing tools + /// A token to cancel the operation + /// A task that completes with the value of the password hashing match. + /// + public static async Task VerifyPasswordAsync(this IUserManager manager, string userId, PrivateString rawPassword, IPasswordHashingProvider hashing, CancellationToken cancellation) { - public readonly Span Buffer { private get; init; } - public readonly Span SignatureBuffer => Buffer[..64]; - - - - public int ClientPbkWritten; - public readonly Span ClientPublicKeyBuffer => Buffer.Slice(64, 1024); - public readonly ReadOnlySpan ClientPbkOutput => ClientPublicKeyBuffer[..ClientPbkWritten]; + _ = userId ?? throw new ArgumentNullException(nameof(userId)); + _ = rawPassword ?? throw new ArgumentNullException(nameof(rawPassword)); + _ = hashing ?? throw new ArgumentNullException(nameof(hashing)); + //Get the user, may be null if the user does not exist + using IUser? user = await manager.GetUserAndPassFromIDAsync(userId, cancellation); - - public int ClientEncBytesWritten; - public readonly Span ClientEncOutputBuffer => Buffer[(64 + 1024)..]; - public readonly ReadOnlySpan EncryptedOutput => ClientEncOutputBuffer[..ClientEncBytesWritten]; + return user != null && hashing.Verify(user.PassHash.ToReadOnlySpan(), rawPassword.ToReadOnlySpan()); } - + /// - /// Computes a random buffer, encrypts it with the client's public key, - /// computes the digest of that key and returns the base64 encoded strings - /// of those components + /// Verifies the user's raw password against the hashed password using the specified + /// instance /// - /// The user's public key credential - /// The base64 encoded digest of the secret that was encrypted - /// The client's user-agent header value - /// A string representing a unique signed token for a given login context - /// - /// - private static void TryGenerateToken(string base64clientPublicKey, out string base64Digest, out string base64ClientData) + /// + /// + /// The provider instance + /// True if the password + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool VerifyPassword(this IUser user, PrivateString rawPassword, IPasswordHashingProvider hashing) { - //Temporary work buffer - using IMemoryHandle buffer = MemoryUtil.SafeAlloc(4096, true); - /* - * Create a new token buffer for bin buffers. - * This buffer struct is used to break up - * a single block of memory into individual - * non-overlapping (important!) buffer windows - * for named purposes - */ - TokenGenBuffers tokenBuf = new() - { - Buffer = buffer.Span - }; - //Recover the clients public key from its base64 encoding - if (!Convert.TryFromBase64String(base64clientPublicKey, tokenBuf.ClientPublicKeyBuffer, out tokenBuf.ClientPbkWritten)) - { - throw new InternalBufferOverflowException("Failed to recover the clients RSA public key"); - } - /* - * Fill signature buffer with random data - * this signature will be stored and used to verify - * signed client messages. It will also be encryped - * using the clients RSA keys - */ - RandomHash.GetRandomBytes(tokenBuf.SignatureBuffer); - /* - * Setup a new RSA Crypto provider that is initialized with the clients - * supplied public key. RSA will be used to encrypt the server secret - * that only the client will be able to decrypt for the current connection - */ - using RSA rsa = RSA.Create(); - //Setup rsa from the users public key - rsa.ImportSubjectPublicKeyInfo(tokenBuf.ClientPbkOutput, out _); - //try to encypte output data - if (!rsa.TryEncrypt(tokenBuf.SignatureBuffer, tokenBuf.ClientEncOutputBuffer, RSAEncryptionPadding.OaepSHA256, out tokenBuf.ClientEncBytesWritten)) - { - throw new InternalBufferOverflowException("Failed to encrypt the server secret"); - } - //Compute the digest of the raw server key - base64Digest = ManagedHash.ComputeBase64Hash(tokenBuf.SignatureBuffer, HashAlg.SHA384); - /* - * The client will send a hash of the decrypted key and will be used - * as a comparison to the hash string above ^ - */ - base64ClientData = Convert.ToBase64String(tokenBuf.EncryptedOutput, Base64FormattingOptions.None); + return user.PassHash != null && hashing.Verify(user.PassHash, rawPassword); } /// - /// Determines if the client sent a token header, and it maches against the current session + /// Verifies a password against its previously encoded hash. /// - /// true if the client set the token header, the session is loaded, and the token matches the session, false otherwise - public static bool TokenMatches(this HttpEntity ev) + /// + /// Previously hashed password + /// Raw password to compare against + /// True if bytes derrived from password match the hash, false otherwise + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Verify(this IPasswordHashingProvider provider, PrivateString passHash, PrivateString password) { - //Get the token from the client header, the client should always sent this - string? clientDigest = ev.Server.Headers[LOGIN_TOKEN_HEADER]; - //Make sure a session is loaded - if (!ev.Session.IsSet || ev.Session.IsNew || string.IsNullOrWhiteSpace(clientDigest)) - { - return false; - } - /* - * Alloc buffer to do conversion and zero initial contents incase the - * payload size has been changed. - * - * The buffer just needs to be large enoguh for the size of the hashes - * that are stored in base64 format. - * - * The values in the buffers will be the raw hash of the client's key - * and the stored key sent during initial authorziation. If the hashes - * are equal it should mean that the client must have the private - * key that generated the public key that was sent - */ - using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(TokenHashSize * 2, true); - //Slice up buffers - Span headerBuffer = buffer.Span[..TokenHashSize]; - Span sessionBuffer = buffer.Span[TokenHashSize..]; - //Convert the header token and the session token - if (Convert.TryFromBase64String(clientDigest, headerBuffer, out int headerTokenLen) - && Convert.TryFromBase64String(ev.Session.Token, sessionBuffer, out int sessionTokenLen)) - { - //Do a fixed time equal (probably overkill, but should not matter too much) - if(CryptographicOperations.FixedTimeEquals(headerBuffer[..headerTokenLen], sessionBuffer[..sessionTokenLen])) - { - return true; - } - } - - /* - * If the token does not match, or cannot be found, check if the client - * has login cookies set, if not remove them. - * - * This does not affect the session, but allows for a web client to update - * its login state if its no-longer logged in - */ - - //Expire login cookie if set - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_NAME)) - { - ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite: CookieSameSite.SameSite); - } - //Expire the LI cookie if set - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_IDENTIFIER)) - { - ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite); - } - - return false; + //Casting PrivateStrings to spans will reference the base string directly + return provider.Verify((ReadOnlySpan)passHash, (ReadOnlySpan)password); } /// - /// Regenerates the user's login token with the public key stored - /// during initial logon + /// Hashes a specified password, with the initialized pepper, and salted with CNG random bytes. /// - /// The base64 of the newly encrypted secret - public static string? RegenerateClientToken(this HttpEntity ev) + /// + /// Password to be hashed + /// + /// A of the hashed and encoded password + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PrivateString Hash(this IPasswordHashingProvider provider, PrivateString password) { - if(!ev.Session.IsSet || ev.Session.SessionType != SessionType.Web) - { - return null; - } - //Get the client's stored public key - string clientPublicKey = ev.Session.GetBrowserPubKey(); - //Make sure its set - if (string.IsNullOrWhiteSpace(clientPublicKey)) - { - return null; - } - //Generate a new token using the stored public key - TryGenerateToken(clientPublicKey, out string base64Digest, out string base64ClientData); - //store the token to the user's session - ev.Session.Token = base64Digest; - //Update the last token upgrade time - ev.Session.LastTokenUpgrade(ev.RequestedTimeUtc); - //return the clients encrypted secret - return base64ClientData; + return provider.Hash((ReadOnlySpan)password); } - /// - /// Tries to encrypt the specified data using the stored public key and store the encrypted data into - /// the output buffer. - /// - /// - /// Data to encrypt - /// The buffer to store encrypted data in - /// - /// The number of encrypted bytes written to the output buffer, - /// or false (0) if the operation failed, or if no credential is - /// stored. - /// - /// - public static ERRNO TryEncryptClientData(this in SessionInfo session, ReadOnlySpan data, in Span outputBuffer) + #endregion + + + + #region Client Auth Extensions + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static IAccountSecurityProvider GetSecProviderOrThrow(this HttpEntity entity) { - if (!session.IsSet) - { - return false; - } - //try to get the public key from the client - string base64PubKey = session.GetBrowserPubKey(); - return TryEncryptClientData(base64PubKey, data, in outputBuffer); + return entity.RequestedRoot.AccountSecurity + ?? throw new NotSupportedException("The processor this connection originated from does not have an account security provider loaded"); } + /// - /// Tries to encrypt the specified data using the specified public key + /// Determines if the current client has the authroziation level to access a given resource /// - /// A base64 encoded public key used to encrypt client data - /// Data to encrypt - /// The buffer to store encrypted data in - /// - /// The number of encrypted bytes written to the output buffer, - /// or false (0) if the operation failed, or if no credential is - /// specified. - /// - /// - public static ERRNO TryEncryptClientData(ReadOnlySpan base64PubKey, ReadOnlySpan data, in Span outputBuffer) + /// + /// The authoziation level + /// True if the connection has the desired authorization status + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsClientAuthorized(this HttpEntity entity, AuthorzationCheckLevel mode = AuthorzationCheckLevel.Critical) { - if (base64PubKey.IsEmpty) - { - return false; - } - //Alloc a buffer for decoding the public key - using UnsafeMemoryHandle pubKeyBuffer = MemoryUtil.UnsafeAlloc(PUBLIC_KEY_BUFFER_SIZE, true); - //Decode the public key - ERRNO pbkBytesWritten = VnEncoding.TryFromBase64Chars(base64PubKey, pubKeyBuffer); - //Try to encrypt the data - return pbkBytesWritten ? TryEncryptClientData(pubKeyBuffer.Span[..(int)pbkBytesWritten], data, in outputBuffer) : false; + IAccountSecurityProvider prov = entity.GetSecProviderOrThrow(); + + return prov.IsClientAuthorized(entity, mode); } + /// - /// Tries to encrypt the specified data using the specified public key + /// Runs necessary operations to grant authorization to the specified user of a given session and user with provided variables /// - /// The raw SKI public key - /// Data to encrypt - /// The buffer to store encrypted data in - /// - /// The number of encrypted bytes written to the output buffer, - /// or false (0) if the operation failed, or if no credential is - /// specified. - /// + /// The connection and session to log-in + /// The clients login security information + /// The user to log-in + /// The encrypted base64 token secret data to send to the client + /// + /// /// - public static ERRNO TryEncryptClientData(ReadOnlySpan rawPubKey, ReadOnlySpan data, in Span outputBuffer) + /// + public static IClientAuthorization GenerateAuthorization(this HttpEntity entity, IClientSecInfo secInfo, IUser user) { - if (rawPubKey.IsEmpty) + _ = secInfo ?? throw new ArgumentNullException(nameof(secInfo)); + + if (!entity.Session.IsSet || entity.Session.SessionType != SessionType.Web) { - return false; + throw new InvalidOperationException("The session is not set or the session is not a web-based session type"); } - //Setup new empty rsa - using RSA rsa = RSA.Create(); - //Import the public key - rsa.ImportSubjectPublicKeyInfo(rawPubKey, out _); - //Encrypt data with OaepSha256 as configured in the browser - return rsa.TryEncrypt(data, outputBuffer, ClientEncryptonPadding, out int bytesWritten) ? bytesWritten : false; - } - /// - /// Stores the clients public key specified during login - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetBrowserPubKey(in SessionInfo session, string base64PubKey) => session[CLIENT_PUB_KEY_ENTRY] = base64PubKey; + IAccountSecurityProvider provider = entity.GetSecProviderOrThrow(); - /// - /// Gets the clients stored public key that was specified during login - /// - /// The base64 encoded public key string specified at login - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetBrowserPubKey(this in SessionInfo session) => session[CLIENT_PUB_KEY_ENTRY]; + //Regen the session id + entity.Session.RegenID(); + + //Authorize client + IClientAuthorization auth = provider.AuthorizeClient(entity, secInfo, user); + + //Clear flags + user.FailedLoginCount(0); + + //Store variables + entity.Session.UserID = user.UserID; + entity.Session.Privilages = user.Privilages; + + //Store client id for later use + entity.Session[BROWSER_ID_ENTRY] = secInfo.ClientId; + + //Get the "local" account flag from the user object + bool localAccount = user.IsLocalAccount(); + + //Set local account flag + entity.Session.HasLocalAccount(localAccount); + + //Return the client encrypted data + return auth; + } /// - /// Stores the login key as a cookie in the current session as long as the session exists - /// / - /// The event to log-in - /// Does the session belong to a local user account + /// Generates a client authorization from the supplied security info + /// using the default and + /// stored the required variables in the + /// response + /// + /// + /// The client's used to authorize the client + /// The user requesting the authenticated use + /// The response to store variables in + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetLogin(this HttpEntity ev, bool? localAccount = null) + public static void GenerateAuthorization(this HttpEntity entity, IClientSecInfo secInfo, IUser user, WebMessage response) { - //Make sure the session is loaded - if (!ev.Session.IsSet) - { - return; - } - string loginString = RandomHash.GetRandomBase64(LOGIN_COOKIE_SIZE); - //Set login cookie and session login hash - ev.Server.SetCookie(LOGIN_COOKIE_NAME, loginString, "", "/", LoginCookieLifespan, CookieSameSite.SameSite, true, true); - ev.Session.LoginHash = loginString; - //If not set get from session storage - localAccount ??= ev.Session.HasLocalAccount(); - //Set the client identifier cookie to a value indicating a local account - ev.Server.SetCookie(LOGIN_COOKIE_IDENTIFIER, localAccount.Value ? "1" : "2", "", "/", LoginCookieLifespan, CookieSameSite.SameSite, false, true); + //Authorize the client + IClientAuthorization auth = GenerateAuthorization(entity, secInfo, user); + + //Set client token + response.Token = auth.SecurityToken.ClientToken; } /// - /// Invalidates the login status of the current connection and session (if session is loaded) + /// Regenerates the client authorization if the client has a currently valid authorization /// + /// + /// The new for the regenerated credentials + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InvalidateLogin(this HttpEntity ev) + public static IClientAuthorization ReAuthorizeClient(this HttpEntity entity) { - //Expire the login cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite: CookieSameSite.SameSite, secure: true); - //Expire the identifier cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite, secure: true); - if (ev.Session.IsSet) - { - //Invalidate the session - ev.Session.Invalidate(); - } + //Get default provider + IAccountSecurityProvider prov = entity.GetSecProviderOrThrow(); + + //Re-authorize the client + return prov.ReAuthorizeClient(entity); } /// - /// Determines if the current session login cookie matches the value stored in the current session (if the session is loaded) + /// Regenerates the client authorization if the client has a currently valid authorization /// - /// True if the session is active, the cookie was properly received, and the cookie value matches the session. False otherwise - public static bool LoginCookieMatches(this HttpEntity ev) - { - //Sessions must be loaded - if (!ev.Session.IsSet) - { - return false; - } - //Try to get the login string from the request cookies - if (!ev.Server.RequestCookies.TryGetNonEmptyValue(LOGIN_COOKIE_NAME, out string? liCookie)) - { - return false; - } - /* - * Alloc buffer to do conversion and zero initial contents incase the - * payload size has been changed. - * - * Since the cookie size and the local copy should be the same size - * and equal to the LOGIN_COOKIE_SIZE constant, the buffer size should - * be 2 * LOGIN_COOKIE_SIZE, and it can be split in half and shared - * for both conversions - */ - using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(2 * LOGIN_COOKIE_SIZE, true); - //Slice up buffers - Span cookieBuffer = buffer.Span[..LOGIN_COOKIE_SIZE]; - Span sessionBuffer = buffer.Span.Slice(LOGIN_COOKIE_SIZE, LOGIN_COOKIE_SIZE); - //Convert cookie and session hash value - if (Convert.TryFromBase64String(liCookie, cookieBuffer, out _) - && Convert.TryFromBase64String(ev.Session.LoginHash, sessionBuffer, out _)) - { - //Do a fixed time equal (probably overkill, but should not matter too much) - if(CryptographicOperations.FixedTimeEquals(cookieBuffer, sessionBuffer)) - { - //If the user is "logged in" and the request is using the POST method, then we can update the cookie - if(ev.Server.Method == HttpMethod.POST && ev.Session.Created.Add(RegenIdPeriod) < ev.RequestedTimeUtc) - { - //Regen login token - ev.SetLogin(); - ev.Session.RegenID(); - } - - return true; - } - } - return false; + /// + /// The response message to return to the client + /// + /// The new for the regenerated credentials + public static void ReAuthorizeClient(this HttpEntity entity, WebMessage response) + { + IAccountSecurityProvider prov = entity.GetSecProviderOrThrow(); + + //Re-authorize the client + IClientAuthorization auth = prov.ReAuthorizeClient(entity); + + //Store the client token in response message + response.Token = auth.SecurityToken.ClientToken; + + //Regen session id also + entity.Session.RegenID(); } - + /// - /// Determines if the client's login cookies need to be updated - /// to reflect its state with the current session's state - /// for the client + /// Attempts to encrypt the supplied data with session stored client information. The user must + /// be authorized /// - /// - public static void ReconcileCookies(this HttpEntity ev) + /// + /// The data to encrypt for the current client + /// The buffer to write encypted data to + /// + /// The number of bytes encrypted and written to the output buffer + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ERRNO TryEncryptClientData(this HttpEntity entity, ReadOnlySpan data, Span output) { - //Only handle cookies if session is loaded and is a web based session - if (!ev.Session.IsSet || ev.Session.SessionType != SessionType.Web) - { - return; - } - if (ev.Session.IsNew) - { - //If either login cookies are set on a new session, clear them - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_NAME) || ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_IDENTIFIER)) - { - //Expire the login cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite:CookieSameSite.SameSite, secure:true); - //Expire the identifier cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite, secure: true); - } - } - //If the session is not supposed to be logged in, clear the login cookies if they were set - else if (string.IsNullOrEmpty(ev.Session.LoginHash)) + //Confirm session is loaded + if(!entity.Session.IsSet || entity.Session.IsNew) { - //If one of either cookie is not set - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_NAME)) - { - //Expire the login cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite: CookieSameSite.SameSite, secure: true); - } - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_IDENTIFIER)) - { - //Expire the identifier cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite, secure: true); - } + return false; } + + //Use the default sec provider + IAccountSecurityProvider prov = entity.GetSecProviderOrThrow(); + return prov.TryEncryptClientData(entity, data, output); } /// - /// Gets the last time the session token was set + /// Attempts to encrypt the supplied data with session stored client information. The user must + /// be authorized /// - /// - /// The last time the token was updated/generated, or if not set - public static DateTimeOffset LastTokenUpgrade(this in SessionInfo session) + /// + /// Used for unauthorized connections to encrypt client data based on client security info + /// The data to encrypt for the current client + /// The buffer to write encypted data to + /// The number of bytes encrypted and written to the output buffer + /// + /// + public static ERRNO TryEncryptClientData(this HttpEntity entity, IClientSecInfo secInfo, ReadOnlySpan data, Span output) { - //Get the serialized time value - string timeString = session[TOKEN_UPDATE_TIME_ENTRY]; - return long.TryParse(timeString, out long time) ? DateTimeOffset.FromUnixTimeSeconds(time) : DateTimeOffset.MinValue; - } + _ = secInfo ?? throw new ArgumentNullException(nameof(secInfo)); - /// - /// Updates the last time the session token was set - /// - /// - /// The UTC time the last token was set - private static void LastTokenUpgrade(this in SessionInfo session, DateTimeOffset updated) - => session[TOKEN_UPDATE_TIME_ENTRY] = updated.ToUnixTimeSeconds().ToString(); + //Use the default sec provider + IAccountSecurityProvider prov = entity.GetSecProviderOrThrow(); + return prov.TryEncryptClientData(secInfo, data, output); + } /// - /// Stores the browser's id during a login process + /// Invalidates the login status of the current connection and session (if session is loaded) /// - /// - /// Browser id value to store + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetBrowserID(in SessionInfo session, string browserId) => session[BROWSER_ID_ENTRY] = browserId; + public static void InvalidateLogin(this HttpEntity entity) + { + //Invalidate against the sec provider + IAccountSecurityProvider prov = entity.GetSecProviderOrThrow(); + + prov.InvalidateLogin(entity); + + //Invalidate the session also + entity.Session.Invalidate(); + } /// /// Gets the current browser's id if it was specified during login process @@ -728,7 +457,8 @@ namespace VNLib.Plugins.Essentials.Accounts /// /// True for a local account, false otherwise [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void HasLocalAccount(this in SessionInfo session, bool value) => session[LOCAL_ACCOUNT_ENTRY] = value ? "1" : null; + public static void HasLocalAccount(this in SessionInfo session, bool value) => session[LOCAL_ACCOUNT_ENTRY] = value ? "1" : null; + /// /// Gets a value indicating if the session belongs to a local user account /// @@ -739,91 +469,6 @@ namespace VNLib.Plugins.Essentials.Accounts #endregion - #region Client Challenge - - /* - * Generates a secret that is used to compute the unique hmac digest of the - * current user's password. The digest is stored in the current session - * and used to compare future requests that require password re-authentication. - * The client will compute the digest of the user's password and send the digest - * instead of the user's password - */ - - /// - /// Generates a new password challenge for the current session and specified password - /// - /// - /// The user's password to compute the hash of - /// The raw derrivation key to send to the client - public static byte[] GenPasswordChallenge(this in SessionInfo session, PrivateString password) - { - ReadOnlySpan rawPass = password; - //Calculate the password buffer size required - int passByteCount = Encoding.UTF8.GetByteCount(rawPass); - //Allocate the buffer - using UnsafeMemoryHandle bufferHandle = MemoryUtil.UnsafeAlloc(passByteCount + 64, true); - //Slice buffers - Span utf8PassBytes = bufferHandle.Span[..passByteCount]; - Span hashBuffer = bufferHandle.Span[passByteCount..]; - //Encode the password into the buffer - _ = Encoding.UTF8.GetBytes(rawPass, utf8PassBytes); - try - { - //Get random secret buffer - byte[] secretKey = RandomHash.GetRandomBytes(SESSION_CHALLENGE_SIZE); - //Compute the digest - int count = HMACSHA512.HashData(secretKey, utf8PassBytes, hashBuffer); - //Store the user's password digest - session[CHALLENGE_HMAC_ENTRY] = VnEncoding.ToBase32String(hashBuffer[..count], false); - return secretKey; - } - finally - { - //Wipe buffer - RandomHash.GetRandomBytes(utf8PassBytes); - } - } - /// - /// Verifies the stored unique digest of the user's password against - /// the client derrived password - /// - /// - /// The base64 client derrived digest of the user's password to verify - /// True if formatting was correct and the derrived passwords match, false otherwise - /// - public static bool VerifyChallenge(this in SessionInfo session, ReadOnlySpan base64PasswordDigest) - { - string base32Digest = session[CHALLENGE_HMAC_ENTRY]; - if (string.IsNullOrWhiteSpace(base32Digest)) - { - return false; - } - int bufSize = base32Digest.Length + base64PasswordDigest.Length; - //Alloc buffer - using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(bufSize); - //Split buffers - Span localBuf = buffer.Span[..base32Digest.Length]; - Span passBuf = buffer.Span[base32Digest.Length..]; - //Recover the stored base32 digest - ERRNO count = VnEncoding.TryFromBase32Chars(base32Digest, localBuf); - if (!count) - { - return false; - } - //Recover base64 bytes - if(!Convert.TryFromBase64Chars(base64PasswordDigest, passBuf, out int passBytesWritten)) - { - return false; - } - //Trim buffers - localBuf = localBuf[..(int)count]; - passBuf = passBuf[..passBytesWritten]; - //Compare and return - return CryptographicOperations.FixedTimeEquals(passBuf, localBuf); - } - - #endregion - #region Privilage Extensions /// /// Compares the users privilage level against the specified level diff --git a/lib/Plugins.Essentials/src/Accounts/AuthorzationCheckLevel.cs b/lib/Plugins.Essentials/src/Accounts/AuthorzationCheckLevel.cs new file mode 100644 index 0000000..aa09bf4 --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/AuthorzationCheckLevel.cs @@ -0,0 +1,56 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: AuthorzationCheckLevel.cs +* +* AuthorzationCheckLevel.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials 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.Essentials 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/. +*/ + + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// Specifies how critical the security check is for a user to access + /// a given resource + /// + public enum AuthorzationCheckLevel + { + /// + /// No authorization check is required. + /// + None, + /// + /// Is there any information that the client may have authorization. NOTE: Not a security check! + /// + Any, + /// + /// The authorization check is not considered criticial, just a basic confirmation + /// that the user should be logged it, but does not need to access secure + /// resources. + /// + Medium, + /// + /// The a full authorization check is required as the user may access + /// secure resouces. + /// + Critical + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/ClientSecurityToken.cs b/lib/Plugins.Essentials/src/Accounts/ClientSecurityToken.cs new file mode 100644 index 0000000..0d4aa58 --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/ClientSecurityToken.cs @@ -0,0 +1,43 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: ClientSecurityToken.cs +* +* ClientSecurityToken.cs is part of VNLib.Plugins.Essentials which is part +* of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials 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.Essentials 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/. +*/ + + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// A structure that contains the client/server information + /// for client/server authorization + /// + /// + /// The public portion of the token to send to the client + /// + /// + /// The secret portion of the token that is to be + /// stored on the server (usually in the client's session) + /// + public readonly record struct ClientSecurityToken(string ClientToken, string ServerToken) + { } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/IAccountSecurityProvider.cs b/lib/Plugins.Essentials/src/Accounts/IAccountSecurityProvider.cs new file mode 100644 index 0000000..c30796b --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/IAccountSecurityProvider.cs @@ -0,0 +1,90 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: IAccountSecurityProvider.cs +* +* IAccountSecurityProvider.cs is part of VNLib.Plugins.Essentials which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials 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.Essentials 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; + +using VNLib.Utils; +using VNLib.Plugins.Essentials.Users; + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// Provides account security to client connections. Providing authoirzation, + /// verification, and client data encryption. + /// + public interface IAccountSecurityProvider + { + /// + /// Generates a new authorization for the connection with its client security information + /// + /// The connection to authorize + /// The client security information required for authorization + /// The user object to authorize the connection for + /// The new authorization information for the connection + IClientAuthorization AuthorizeClient(HttpEntity entity, IClientSecInfo clientInfo, IUser user); + + /// + /// Regenerates the client's authorization status for a currently logged-in user + /// + /// The connection to re-authorize + /// The new containing the new authorization information + IClientAuthorization ReAuthorizeClient(HttpEntity entity); + + /// + /// Determines if the connection is considered authorized for the desired + /// security level + /// + /// The connection to determine the status of + /// The authorziation level to check for + /// True if the given connection meets the desired authorzation status + bool IsClientAuthorized(HttpEntity entity, AuthorzationCheckLevel level); + + /// + /// Encryptes data using the stored client's authorization information. + /// + /// The connection to encrypt data for + /// The data to encrypt + /// The buffer to write the encrypted data to + /// The number of bytes written to the output buffer, or o/false if the data could not be encrypted + ERRNO TryEncryptClientData(HttpEntity entity, ReadOnlySpan data, Span outputBuffer); + + /// + /// Attempts a one-time encryption of client data for a non-authorized user + /// based on the client's data. + /// + /// The client's credentials used to encrypt the message + /// The data to encrypt + /// The output buffer to write encrypted data to + /// The number of bytes written to the output buffer, 0/false if the operation failed + ERRNO TryEncryptClientData(IClientSecInfo clientSecInfo, ReadOnlySpan data, Span outputBuffer); + + /// + /// Invalidates a logged in connection + /// + /// The connection to invalidate the login status of + void InvalidateLogin(HttpEntity entity); + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/IClientAuthorization.cs b/lib/Plugins.Essentials/src/Accounts/IClientAuthorization.cs new file mode 100644 index 0000000..02bc96e --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/IClientAuthorization.cs @@ -0,0 +1,45 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: IClientAuthorization.cs +* +* IClientAuthorization.cs is part of VNLib.Plugins.Essentials which is +* part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials 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.Essentials 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/. +*/ + + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// Contains the client's minimum authorization variables + /// + public interface IClientAuthorization + { + /// + /// A security token that may be set as a cookie or used + /// + string? LoginSecurityString { get; } + + /// + /// The clients security token information + /// + ClientSecurityToken SecurityToken { get; } + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/IClientSecInfo.cs b/lib/Plugins.Essentials/src/Accounts/IClientSecInfo.cs new file mode 100644 index 0000000..6990191 --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/IClientSecInfo.cs @@ -0,0 +1,46 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: AccountUtil.cs +* +* AccountUtil.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials 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.Essentials 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/. +*/ + + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// Exposed the required security information for a + /// to authorized a connection. + /// + public interface IClientSecInfo + { + /// + /// The clients public-key + /// + string PublicKey { get; } + + /// + /// The unique id the client provided to this server + /// + string ClientId { get; } + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/IPasswordHashingProvider.cs b/lib/Plugins.Essentials/src/Accounts/IPasswordHashingProvider.cs new file mode 100644 index 0000000..fc45727 --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/IPasswordHashingProvider.cs @@ -0,0 +1,80 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: IPasswordHashingProvider.cs +* +* IPasswordHashingProvider.cs is part of VNLib.Plugins.Essentials which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials 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.Essentials 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; + +using VNLib.Utils; +using VNLib.Utils.Memory; + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// Represents a common abstraction for password hashing providers/libraries + /// + public interface IPasswordHashingProvider + { + /// + /// Verifies a password against its previously encoded hash. + /// + /// Previously hashed password + /// Raw password to compare against + /// true if bytes derrived from password match the hash, false otherwise + /// + bool Verify(ReadOnlySpan passHash, ReadOnlySpan password); + + /// + /// Verifies a password against its previously encoded hash. + /// + /// Previously hashed password in binary + /// Raw password to compare against the hash + /// true if bytes derrived from password match the hash, false otherwise + /// + bool Verify(ReadOnlySpan passHash, ReadOnlySpan password); + + /// + /// Hashes the specified character encoded password to it's secured hashed form. + /// + /// The character encoded password to encrypt + /// A containing the new password hash. + /// + PrivateString Hash(ReadOnlySpan password); + + /// + /// Hashes the specified binary encoded password to it's secured hashed form. + /// + /// The binary encoded password to encrypt + /// A containing the new password hash. + /// + PrivateString Hash(ReadOnlySpan password); + + /// + /// Exposes a lower level for producing a password hash and writing it to the output buffer + /// + /// The raw password to encrypt + /// The output buffer to write encoded data into + /// The number of bytes written to the hash buffer, or 0/false if the hashing operation failed + /// + ERRNO Hash(ReadOnlySpan password, Span hashOutput); + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/LoginMessage.cs b/lib/Plugins.Essentials/src/Accounts/LoginMessage.cs index ebc616e..96bf261 100644 --- a/lib/Plugins.Essentials/src/Accounts/LoginMessage.cs +++ b/lib/Plugins.Essentials/src/Accounts/LoginMessage.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -36,7 +36,7 @@ namespace VNLib.Plugins.Essentials.Accounts /// NOTE: This class derrives from /// and should be disposed properly /// - public class LoginMessage : PrivateStringManager + public class LoginMessage : PrivateStringManager, IClientSecInfo { /// /// A property @@ -80,7 +80,8 @@ namespace VNLib.Plugins.Essentials.Accounts /// The clients browser id if shared /// [JsonPropertyName("clientid")] - public string ClientID { get; set; } + public string ClientId { get; set; } + /// /// Initailzies a new and its parent /// base @@ -98,5 +99,10 @@ namespace VNLib.Plugins.Essentials.Accounts /// or access to will throw /// protected LoginMessage(int protectedElementSize = 1) : base(protectedElementSize) { } + + /* + * Support client security info + */ + string IClientSecInfo.PublicKey => ClientPublicKey; } } \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/PasswordChallengeResult.cs b/lib/Plugins.Essentials/src/Accounts/PasswordChallengeResult.cs new file mode 100644 index 0000000..3ad05ab --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/PasswordChallengeResult.cs @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: PasswordChallengeResult.cs +* +* PasswordChallengeResult.cs is part of VNLib.Plugins.Essentials which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials 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.Essentials 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; + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// A password pased client/server challenge + /// + /// The client portion of the password based challenge + /// The server potion of the password based challenge + public readonly record struct PasswordChallengeResult(string ClientData, string ServerData) + { } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs b/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs index 553b41c..db5b309 100644 --- a/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs +++ b/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -31,14 +31,17 @@ using VNLib.Utils.Memory; namespace VNLib.Plugins.Essentials.Accounts { + /// /// Provides a structured password hashing system implementing the library /// with fixed time comparison /// - public sealed class PasswordHashing + public sealed class PasswordHashing : IPasswordHashingProvider { + private const int STACK_MAX_BUFF_SIZE = 64; + private readonly ISecretProvider _secret; - + private readonly uint TimeCost; private readonly uint MemoryCost; private readonly uint HashLen; @@ -71,53 +74,50 @@ namespace VNLib.Plugins.Essentials.Accounts SaltLen = saltLen; Parallelism = parallism < 1 ? (uint)Environment.ProcessorCount : parallism; } - - /// - /// Verifies a password against its previously encoded hash. - /// - /// Previously hashed password - /// Raw password to compare against - /// true if bytes derrived from password match the hash, false otherwise - /// - /// - /// - public bool Verify(PrivateString passHash, PrivateString password) - { - //Casting PrivateStrings to spans will reference the base string directly - return Verify((ReadOnlySpan)passHash, (ReadOnlySpan)password); - } - /// - /// Verifies a password against its previously encoded hash. - /// - /// Previously hashed password - /// Raw password to compare against - /// true if bytes derrived from password match the hash, false otherwise - /// - /// - /// + + /// + /// + /// public bool Verify(ReadOnlySpan passHash, ReadOnlySpan password) { if(passHash.IsEmpty || password.IsEmpty) { return false; } - //alloc secret buffer - using UnsafeMemoryHandle secretBuffer = MemoryUtil.UnsafeAlloc(_secret.BufferSize, true); + + if(_secret.BufferSize < STACK_MAX_BUFF_SIZE) + { + //Alloc stack buffer + Span secretBuffer = stackalloc byte[STACK_MAX_BUFF_SIZE]; + + return VerifyInternal(passHash, password, secretBuffer); + } + else + { + //Alloc heap buffer + using UnsafeMemoryHandle secretBuffer = MemoryUtil.UnsafeAlloc(_secret.BufferSize, true); + + return VerifyInternal(passHash, password, secretBuffer); + } + } + + private bool VerifyInternal(ReadOnlySpan passHash, ReadOnlySpan password, Span secretBuffer) + { try { //Get the secret from the callback - ERRNO count = _secret.GetSecret(secretBuffer.Span); + ERRNO count = _secret.GetSecret(secretBuffer); //Verify - return VnArgon2.Verify2id(password, passHash, secretBuffer.Span[..(int)count]); + return VnArgon2.Verify2id(password, passHash, secretBuffer[..(int)count]); } finally { //Erase secret buffer - MemoryUtil.InitializeBlock(secretBuffer.Span); + MemoryUtil.InitializeBlock(secretBuffer); } } - + /// /// Verifies a password against its hash. Partially exposes the Argon2 api. /// @@ -136,19 +136,8 @@ namespace VNLib.Plugins.Essentials.Accounts //Compare the hashed password to the specified hash and return results return CryptographicOperations.FixedTimeEquals(hash, hashBuf.Span); } - - /// - /// Hashes a specified password, with the initialized pepper, and salted with CNG random bytes. - /// - /// Password to be hashed - /// - /// A of the hashed and encoded password - public PrivateString Hash(PrivateString password) => Hash((ReadOnlySpan)password); - /// - /// Hashes a specified password, with the initialized pepper, and salted with CNG random bytes. - /// - /// Password to be hashed + /// /// /// A of the hashed and encoded password public PrivateString Hash(ReadOnlySpan password) @@ -175,11 +164,8 @@ namespace VNLib.Plugins.Essentials.Accounts MemoryUtil.InitializeBlock(buffer.Span); } } - - /// - /// Hashes a specified password, with the initialized pepper, and salted with a CNG random bytes. - /// - /// Password to be hashed + + /// /// /// A of the hashed and encoded password public PrivateString Hash(ReadOnlySpan password) @@ -231,5 +217,76 @@ namespace VNLib.Plugins.Essentials.Accounts MemoryUtil.InitializeBlock(secretBuffer.Span); } } + + /// + /// NOT SUPPORTED! Use + /// instead to specify the salt that was used to encypt the original password + /// + /// + /// + /// + public bool Verify(ReadOnlySpan passHash, ReadOnlySpan password) + { + throw new NotSupportedException(); + } + + /// + /// + public ERRNO Hash(ReadOnlySpan password, Span hashOutput) + { + //Calc the min buffer size + int minBufferSize = SaltLen + _secret.BufferSize + (int)HashLen; + + //Alloc heap buffer + using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAllocNearestPage(minBufferSize, true); + try + { + //Segment the buffer + HashBufferSegments segments = new(buffer.Span, _secret.BufferSize, SaltLen, (int)HashLen); + + //Fill the buffer with random bytes + RandomHash.GetRandomBytes(segments.SaltBuffer); + + //recover the secret + ERRNO count = _secret.GetSecret(segments.SecretBuffer); + + //Hash the password in binary and write the secret to the binary buffer + VnArgon2.Hash2id(password, segments.SaltBuffer, segments.SecretBuffer[..(int)count], segments.HashBuffer, TimeCost, MemoryCost, Parallelism); + + //Hash size is the desired hash size + return new((int)HashLen); + } + finally + { + MemoryUtil.InitializeBlock(buffer.Span); + } + } + + private readonly ref struct HashBufferSegments + { + public readonly Span SaltBuffer; + + public readonly Span SecretBuffer; + + public readonly Span HashBuffer; + + public HashBufferSegments(Span buffer, int secretSize, int saltSize, int hashSize) + { + //Salt buffer is begining segment + SaltBuffer = buffer[..saltSize]; + + //Shift to end of salt buffer + buffer = buffer[saltSize..]; + + //Store secret buffer + SecretBuffer = buffer[..secretSize]; + + //Shift to end of secret buffer + buffer = buffer[secretSize..]; + + //Store remaining size as hash buffer + HashBuffer = buffer[..hashSize]; + } + } } } \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs b/lib/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs index bced960..c529028 100644 --- a/lib/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs +++ b/lib/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -36,6 +36,12 @@ namespace VNLib.Plugins.Essentials.Endpoints /// public abstract class ProtectedWebEndpoint : UnprotectedWebEndpoint { + /// + /// Gets the minium required by a client to + /// access this endpoint + /// + protected virtual AuthorzationCheckLevel AuthLevel { get; } = AuthorzationCheckLevel.Critical; + /// protected override ERRNO PreProccess(HttpEntity entity) { @@ -43,14 +49,16 @@ namespace VNLib.Plugins.Essentials.Endpoints { return false; } - //The loggged in flag must be set, and the token must also match - if (!entity.LoginCookieMatches() || !entity.TokenMatches()) + + //Require full authorization to the resource + if (!entity.IsClientAuthorized(AuthLevel)) { //Return unauthorized status entity.CloseResponse(HttpStatusCode.Unauthorized); //A return value less than 0 signals a virtual skip event return -1; } + //Continue return true; } diff --git a/lib/Plugins.Essentials/src/EventProcessor.cs b/lib/Plugins.Essentials/src/EventProcessor.cs index d34cf95..ccaa1a1 100644 --- a/lib/Plugins.Essentials/src/EventProcessor.cs +++ b/lib/Plugins.Essentials/src/EventProcessor.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -38,18 +38,20 @@ using VNLib.Utils.Resources; using VNLib.Plugins.Essentials.Content; using VNLib.Plugins.Essentials.Sessions; using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Essentials.Accounts; #nullable enable namespace VNLib.Plugins.Essentials { + /// /// Provides an abstract base implementation of /// that breaks down simple processing procedures, routing, and session /// loading. /// - public abstract class EventProcessor : IWebRoot + public abstract class EventProcessor : IWebRoot, IWebProcessor { private static readonly AsyncLocal _currentProcessor = new(); @@ -70,6 +72,9 @@ namespace VNLib.Plugins.Essentials /// public abstract IEpProcessingOptions Options { get; } + /// + public abstract IReadOnlyDictionary Redirects { get; } + /// /// Event log provider /// @@ -112,23 +117,10 @@ namespace VNLib.Plugins.Essentials /// The selected file processing routine for the given request public abstract void PostProcessFile(HttpEntity entity, in FileProcessArgs chosenRoutine); - #region redirects - /// - public IReadOnlyDictionary Redirects => _redirects; - - private Dictionary _redirects = new(); + #region security - /// - /// Initializes 301 redirects table from a collection of redirects - /// - /// A collection of redirects - public void SetRedirects(IEnumerable redirs) - { - //To dictionary - Dictionary r = redirs.ToDictionary(r => r.Url, r => r, StringComparer.OrdinalIgnoreCase); - //Swap - _ = Interlocked.Exchange(ref _redirects, r); - } + /// + public abstract IAccountSecurityProvider AccountSecurity { get; } #endregion @@ -204,7 +196,7 @@ namespace VNLib.Plugins.Essentials /// A "lookup table" that represents virtual endpoints to be processed when an /// incomming connection matches its path parameter /// - private Dictionary> VirtualEndpoints = new(); + private IReadOnlyDictionary> VirtualEndpoints = new Dictionary>(); /* @@ -271,7 +263,7 @@ namespace VNLib.Plugins.Essentials { _ = eps ?? throw new ArgumentNullException(nameof(eps)); //Call remove on path - RemoveVirtualEndpoint(eps.Select(static s => s.Path).ToArray()); + RemoveEndpoint(eps.Select(static s => s.Path).ToArray()); } /// @@ -281,9 +273,10 @@ namespace VNLib.Plugins.Essentials /// /// /// - public void RemoveVirtualEndpoint(params string[] paths) + public void RemoveEndpoint(params string[] paths) { _ = paths ?? throw new ArgumentNullException(nameof(paths)); + //Make sure all endpoints specify a path if (paths.Any(static e => string.IsNullOrWhiteSpace(e))) { diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs index 4fd77a6..4179f74 100644 --- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs +++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -322,10 +322,10 @@ namespace VNLib.Plugins.Essentials.Extensions /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, in ReadOnlySpan data) + public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data) { //Get a memory stream using UTF8 encoding - CloseResponse(ev, code, type, in data, ev.Server.Encoding); + CloseResponse(ev, code, type, data, ev.Server.Encoding); } /// @@ -338,7 +338,7 @@ namespace VNLib.Plugins.Essentials.Extensions /// The encoding type to use when converting the buffer /// This method will store an encoded copy as a memory stream, so be careful with large buffers [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, in ReadOnlySpan data, Encoding encoding) + public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data, Encoding encoding) { if (data.IsEmpty) { @@ -479,7 +479,7 @@ namespace VNLib.Plugins.Essentials.Extensions try { //Deserialize and return the object - obj = value.AsJsonObject(options); + obj = JsonSerializer.Deserialize(value, options); return true; } catch(JsonException je) @@ -543,7 +543,7 @@ namespace VNLib.Plugins.Essentials.Extensions try { //Beware this will buffer the entire file object before it attmepts to de-serialize it - return VnEncoding.JSONDeserializeFromBinary(file.FileData, options); + return JsonSerializer.Deserialize(file.FileData, options); } catch (JsonException je) { diff --git a/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs b/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs index d7f73a3..332e3d6 100644 --- a/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs +++ b/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs @@ -37,11 +37,33 @@ namespace VNLib.Plugins.Essentials.Extensions /// The cookie value public readonly record struct HttpCookie (string Name, string Value) { + /// + /// The length of time the cookie is valid for + /// public readonly TimeSpan ValidFor { get; init; } = TimeSpan.MaxValue; - public readonly string Domain { get; init; } = ""; - public readonly string Path { get; init; } = "/"; + /// + /// The cookie's domain parameter. If null, is not set in the + /// Set-Cookie header. + /// + public readonly string? Domain { get; init; } = null; + /// + /// The cookies path parameter. If null, is not + /// set in the Set-Cookie header. + /// + public readonly string? Path { get; init; } = "/"; + /// + /// The cookie's same-site parameter. Default is + /// public readonly CookieSameSite SameSite { get; init; } = CookieSameSite.None; + /// + /// Sets the cookie's HttpOnly parameter. Default is false. When false, does not + /// set the HttpOnly paramter in the Set-Cookie header. + /// public readonly bool HttpOnly { get; init; } = false; + /// + /// Sets the cookie's Secure parameter. Default is false. When false, does not + /// set the Secure parameter in the Set-Cookie header. + /// public readonly bool Secure { get; init; } = false; /// diff --git a/lib/Plugins.Essentials/src/HttpEntity.cs b/lib/Plugins.Essentials/src/HttpEntity.cs index 63e61f7..f2f9387 100644 --- a/lib/Plugins.Essentials/src/HttpEntity.cs +++ b/lib/Plugins.Essentials/src/HttpEntity.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -121,7 +121,7 @@ namespace VNLib.Plugins.Essentials /// /// The requested web root. Provides additional site information /// - public readonly EventProcessor RequestedRoot; + public readonly IWebProcessor RequestedRoot; /// /// If the request has query arguments they are stored in key value format /// diff --git a/lib/Plugins.Essentials/src/IEpProcessingOptions.cs b/lib/Plugins.Essentials/src/IEpProcessingOptions.cs index de79327..13dcd37 100644 --- a/lib/Plugins.Essentials/src/IEpProcessingOptions.cs +++ b/lib/Plugins.Essentials/src/IEpProcessingOptions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -27,6 +27,8 @@ using System.IO; using System.Net; using System.Collections.Generic; +using VNLib.Net.Http; + #nullable enable namespace VNLib.Plugins.Essentials @@ -62,5 +64,9 @@ namespace VNLib.Plugins.Essentials /// A for how long a connection may remain open before all operations are cancelled /// TimeSpan ExecutionTimeout { get; } + /// + /// HTTP level "hard" 301 redirects + /// + IReadOnlyDictionary HardRedirects { get; } } } \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/IWebProcessorInfo.cs b/lib/Plugins.Essentials/src/IWebProcessorInfo.cs new file mode 100644 index 0000000..93a9211 --- /dev/null +++ b/lib/Plugins.Essentials/src/IWebProcessorInfo.cs @@ -0,0 +1,82 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: IWebProcessor.cs +* +* IWebProcessor.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials 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.Essentials 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/. +*/ + + +#nullable enable + +using VNLib.Net.Http; +using VNLib.Plugins.Essentials.Accounts; + +namespace VNLib.Plugins.Essentials +{ + /// + /// Abstractions for methods and information for processors + /// + public interface IWebProcessor : IWebRoot + { + /// + /// The filesystem entrypoint path for the site + /// + string Directory { get; } + + /// + /// Gets the EP processing options + /// + IEpProcessingOptions Options { get; } + + /// + /// The shared that provides + /// user account security operations + /// + IAccountSecurityProvider AccountSecurity { get; } + + /// + /// + /// Called when the server intends to process a file and requires translation from a + /// uri path to a usable filesystem path + /// + /// + /// NOTE: This function must be thread-safe! + /// + /// + /// The path requested by the request + /// The translated and filtered filesystem path used to identify the file resource + string TranslateResourcePath(string requestPath); + + /// + /// Finds the file specified by the request and the server root the user has requested. + /// Determines if it exists, has permissions to access it, and allowed file attributes. + /// Also finds default files and files without extensions + /// + bool FindResourceInRoot(string resourcePath, bool fullyQualified, out string path); + + /// + /// Determines if a requested resource exists within the and is allowed to be accessed. + /// + /// The path to the resource + /// An out parameter that is set to the absolute path to the existing and accessable resource + /// True if the resource exists and is allowed to be accessed + bool FindResourceInRoot(string resourcePath, out string path); + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs b/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs index 892a24c..11ab61a 100644 --- a/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs +++ b/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -22,13 +22,12 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; using System.Net; -using System.Text; using VNLib.Net.Http; -using VNLib.Utils; -using VNLib.Utils.IO; -using VNLib.Utils.Memory.Caching; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Extensions; namespace VNLib.Plugins.Essentials.Oauth @@ -74,11 +73,6 @@ namespace VNLib.Plugins.Essentials.Oauth public static class OauthHttpExtensions { - private static ThreadLocalObjectStorage SbRental { get; } = ObjectRental.CreateThreadLocal(Constructor, null, ReturnFunc); - - private static StringBuilder Constructor() => new(64); - private static void ReturnFunc(StringBuilder sb) => sb.Clear(); - /// /// Closes the current response with a json error message with the message details /// @@ -86,134 +80,53 @@ namespace VNLib.Plugins.Essentials.Oauth /// The http status code /// The short error /// The error description message - public static void CloseResponseError(this HttpEntity ev, HttpStatusCode code, ErrorType error, string description) + public static void CloseResponseError(this IHttpEvent ev, HttpStatusCode code, ErrorType error, ReadOnlySpan description) { //See if the response accepts json if (ev.Server.Accepts(ContentType.Json)) { - //Use a stringbuilder to create json result for the error description - StringBuilder sb = SbRental.Rent(); - sb.Append("{\"error\":\""); - switch (error) - { - case ErrorType.InvalidRequest: - sb.Append("invalid_request"); - break; - case ErrorType.InvalidClient: - sb.Append("invalid_client"); - break; - case ErrorType.UnauthorizedClient: - sb.Append("unauthorized_client"); - break; - case ErrorType.InvalidToken: - sb.Append("invalid_token"); - break; - case ErrorType.UnsupportedResponseType: - sb.Append("unsupported_response_type"); - break; - case ErrorType.InvalidScope: - sb.Append("invalid_scope"); - break; - case ErrorType.ServerError: - sb.Append("server_error"); - break; - case ErrorType.TemporarilyUnabavailable: - sb.Append("temporarily_unavailable"); - break; - default: - sb.Append("error"); - break; - } - sb.Append("\",\"error_description\":\""); - sb.Append(description); - sb.Append("\"}"); - //Close the response with the json data - ev.CloseResponse(code, ContentType.Json, sb.ToString()); - //Return the builder - SbRental.Return(sb); - } - //Otherwise set the error code in the wwwauth header - else - { - //Set the error result in the header - ev.Server.Headers[HttpResponseHeader.WwwAuthenticate] = error switch - { - ErrorType.InvalidRequest => $"Bearer error=\"invalid_request\"", - ErrorType.UnauthorizedClient => $"Bearer error=\"unauthorized_client\"", - ErrorType.UnsupportedResponseType => $"Bearer error=\"unsupported_response_type\"", - ErrorType.InvalidScope => $"Bearer error=\"invalid_scope\"", - ErrorType.ServerError => $"Bearer error=\"server_error\"", - ErrorType.TemporarilyUnabavailable => $"Bearer error=\"temporarily_unavailable\"", - ErrorType.InvalidClient => $"Bearer error=\"invalid_client\"", - ErrorType.InvalidToken => $"Bearer error=\"invalid_token\"", - _ => $"Bearer error=\"error\"", - }; - //Close the response with the status code - ev.CloseResponse(code); - } - } - /// - /// Closes the current response with a json error message with the message details - /// - /// - /// The http status code - /// The short error - /// The error description message - public static void CloseResponseError(this IHttpEvent ev, HttpStatusCode code, ErrorType error, string description) - { - //See if the response accepts json - if (ev.Server.Accepts(ContentType.Json)) - { - //Use a stringbuilder to create json result for the error description - StringBuilder sb = SbRental.Rent(); - sb.Append("{\"error\":\""); + //Alloc char buffer to write output to, nearest page should give us enough room + using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAllocNearestPage(description.Length + 64); + ForwardOnlyWriter writer = new(buffer.Span); + + //Build the error message string + writer.Append("{\"error\":\""); switch (error) { case ErrorType.InvalidRequest: - sb.Append("invalid_request"); + writer.Append("invalid_request"); break; case ErrorType.InvalidClient: - sb.Append("invalid_client"); + writer.Append("invalid_client"); break; case ErrorType.UnauthorizedClient: - sb.Append("unauthorized_client"); + writer.Append("unauthorized_client"); break; case ErrorType.InvalidToken: - sb.Append("invalid_token"); + writer.Append("invalid_token"); break; case ErrorType.UnsupportedResponseType: - sb.Append("unsupported_response_type"); + writer.Append("unsupported_response_type"); break; case ErrorType.InvalidScope: - sb.Append("invalid_scope"); + writer.Append("invalid_scope"); break; case ErrorType.ServerError: - sb.Append("server_error"); + writer.Append("server_error"); break; case ErrorType.TemporarilyUnabavailable: - sb.Append("temporarily_unavailable"); + writer.Append("temporarily_unavailable"); break; default: - sb.Append("error"); + writer.Append("error"); break; } - sb.Append("\",\"error_description\":\""); - sb.Append(description); - sb.Append("\"}"); + writer.Append("\",\"error_description\":\""); + writer.Append(description); + writer.Append("\"}"); - VnMemoryStream vms = VnEncoding.GetMemoryStream(sb.ToString(), ev.Server.Encoding); - try - { - //Close the response with the json data - ev.CloseResponse(code, ContentType.Json, vms); - } - catch - { - vms.Dispose(); - throw; - } - //Return the builder - SbRental.Return(sb); + //Close the response with the json data + ev.CloseResponse(code, ContentType.Json, writer.AsSpan()); } //Otherwise set the error code in the wwwauth header else diff --git a/lib/Plugins.Essentials/src/Sessions/SessionBase.cs b/lib/Plugins.Essentials/src/Sessions/SessionBase.cs index c8cab0d..46e6ec8 100644 --- a/lib/Plugins.Essentials/src/Sessions/SessionBase.cs +++ b/lib/Plugins.Essentials/src/Sessions/SessionBase.cs @@ -26,9 +26,7 @@ using System; using System.Net; using System.Runtime.CompilerServices; -using VNLib.Net.Http; using VNLib.Utils; -using VNLib.Utils.Async; namespace VNLib.Plugins.Essentials.Sessions { @@ -36,7 +34,7 @@ namespace VNLib.Plugins.Essentials.Sessions /// Provides a base class for the interface for exclusive use within a multithreaded /// context /// - public abstract class SessionBase : AsyncExclusiveResource, ISession + public abstract class SessionBase : ISession { protected const ulong MODIFIED_MSK = 0b0000000000000001UL; protected const ulong IS_NEW_MSK = 0b0000000000000010UL; @@ -68,24 +66,16 @@ namespace VNLib.Plugins.Essentials.Sessions } /// - public virtual string SessionID { get; protected set; } + public abstract string SessionID { get; } /// - public virtual DateTimeOffset Created { get; protected set; } + public abstract DateTimeOffset Created { get; set; } /// /// public string this[string index] { - get - { - Check(); - return IndexerGet(index); - } - set - { - Check(); - IndexerSet(index, value); - } + get => IndexerGet(index); + set => IndexerSet(index, value); } /// public virtual IPAddress UserIP diff --git a/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs b/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs index 816fa94..a5d7c4d 100644 --- a/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs +++ b/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -24,14 +24,15 @@ using System; using System.Net; +using System.Text.Json; using System.Security.Authentication; using System.Runtime.CompilerServices; using VNLib.Utils; using VNLib.Net.Http; -using VNLib.Utils.Extensions; using static VNLib.Plugins.Essentials.Statics; + /* * SessionInfo is a structure since it is only meant used in * an HttpEntity context, so it may be allocated as part of @@ -43,6 +44,8 @@ using static VNLib.Plugins.Essentials.Statics; #pragma warning disable CA1051 // Do not declare visible instance fields +#nullable enable + namespace VNLib.Plugins.Essentials.Sessions { /// @@ -55,13 +58,46 @@ namespace VNLib.Plugins.Essentials.Sessions /// public readonly struct SessionInfo : IObjectStorage, IEquatable { + /* + * Store status flags as a 1 byte enum + */ + [Flags] + private enum SessionFlags : byte + { + None = 0x00, + IsSet = 0x01, + IpMatch = 0x02, + IsCrossOrigin = 0x04, + CrossOriginMatch = 0x08, + } + + private readonly ISession UserSession; + private readonly SessionFlags _flags; + /// /// A value indicating if the current instance has been initiailzed /// with a session. Otherwise properties are undefied /// - public readonly bool IsSet; + public readonly bool IsSet + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _flags.HasFlag(SessionFlags.IsSet); + } - private readonly ISession UserSession; + /// + /// The origin header specified during session creation + /// + public readonly Uri? SpecifiedOrigin; + + /// + /// Was the session Initialy established on a secure connection? + /// + public readonly SslProtocols SecurityProcol; + + /// + /// Session stored User-Agent + /// + public readonly string? UserAgent; /// /// Key that identifies the current session. (Identical to cookie::sessionid) @@ -71,51 +107,52 @@ namespace VNLib.Plugins.Essentials.Sessions [MethodImpl(MethodImplOptions.AggressiveInlining)] get => UserSession.SessionID; } - /// - /// Session stored User-Agent - /// - public readonly string UserAgent; + /// /// If the stored IP and current user's IP matches /// - public readonly bool IPMatch; + public readonly bool IPMatch + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _flags.HasFlag(SessionFlags.IpMatch); + } + /// /// If the current connection and stored session have matching cross origin domains /// - public readonly bool CrossOriginMatch; - /// - /// Flags the session as invalid. IMPORTANT: the user's session data is no longer valid and will throw an exception when accessed - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Invalidate(bool all = false) => UserSession.Invalidate(all); - /// - /// Marks the session ID to be regenerated during closing event - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RegenID() => UserSession.RegenID(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T GetObject(string key) => this[key].AsJsonObject(SR_OPTIONS); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetObject(string key, T obj) => this[key] = obj?.ToJsonString(SR_OPTIONS); + public readonly bool CrossOriginMatch + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _flags.HasFlag(SessionFlags.CrossOriginMatch); + } /// /// Was the original session cross origin? /// - public readonly bool CrossOrigin; + public readonly bool CrossOrigin + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _flags.HasFlag(SessionFlags.IsCrossOrigin); + } + /// - /// The origin header specified during session creation + /// Was this session just created on this connection? /// - public readonly Uri SpecifiedOrigin; + public readonly bool IsNew + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => UserSession.IsNew; + } + /// /// The time the session was created /// - public readonly DateTimeOffset Created; - /// - /// Was this session just created on this connection? - /// - public readonly bool IsNew; + public readonly DateTimeOffset Created + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => UserSession.Created; + } + /// /// Gets or sets the session's login hash, if set to a non-empty/null value, will trigger an upgrade on close /// @@ -126,6 +163,7 @@ namespace VNLib.Plugins.Essentials.Sessions [MethodImpl(MethodImplOptions.AggressiveInlining)] set => UserSession.SetLoginToken(value); } + /// /// Gets or sets the session's login token, if set to a non-empty/null value, will trigger an upgrade on close /// @@ -136,6 +174,7 @@ namespace VNLib.Plugins.Essentials.Sessions [MethodImpl(MethodImplOptions.AggressiveInlining)] set => UserSession.Token = value; } + /// /// /// Gets or sets the user-id for the current session. @@ -151,6 +190,7 @@ namespace VNLib.Plugins.Essentials.Sessions [MethodImpl(MethodImplOptions.AggressiveInlining)] set => UserSession.UserID = value; } + /// /// Privilages associated with user specified during login /// @@ -161,6 +201,7 @@ namespace VNLib.Plugins.Essentials.Sessions [MethodImpl(MethodImplOptions.AggressiveInlining)] set => UserSession.Privilages = value; } + /// /// The IP address belonging to the client /// @@ -168,11 +209,8 @@ namespace VNLib.Plugins.Essentials.Sessions { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => UserSession.UserIP; - } - /// - /// Was the session Initialy established on a secure connection? - /// - public readonly SslProtocols SecurityProcol; + } + /// /// A value specifying the type of the backing session /// @@ -182,6 +220,27 @@ namespace VNLib.Plugins.Essentials.Sessions get => UserSession.SessionType; } + /// + /// Flags the session as invalid. IMPORTANT: the user's session data is no longer valid, no data + /// will be saved to the session store when the session closes + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invalidate(bool all = false) => UserSession.Invalidate(all); + /// + /// Marks the session ID to be regenerated during closing event + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegenID() => UserSession.RegenID(); + +#nullable disable + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T GetObject(string key) => JsonSerializer.Deserialize(this[key], SR_OPTIONS); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetObject(string key, T obj) => this[key] = obj == null ? null: JsonSerializer.Serialize(obj, SR_OPTIONS); +#nullable enable + /// /// Accesses the session's general storage /// @@ -198,37 +257,45 @@ namespace VNLib.Plugins.Essentials.Sessions internal SessionInfo(ISession session, IConnectionInfo ci, IPAddress trueIp) { UserSession = session; - //Calculate and store - IsNew = session.IsNew; - Created = session.Created; - //Ip match - IPMatch = trueIp.Equals(session.UserIP); + + SessionFlags flags = SessionFlags.IsSet; + + //Set ip match flag if current ip and stored ip match + flags |= trueIp.Equals(session.UserIP) ? SessionFlags.IpMatch : SessionFlags.None; + //If the session is new, we can store intial security variables if (session.IsNew) { session.InitNewSession(ci); + //Since all values will be the same as the connection, cache the connection values UserAgent = ci.UserAgent; SpecifiedOrigin = ci.Origin; - CrossOrigin = ci.CrossOrigin; SecurityProcol = ci.SecurityProtocol; + + flags |= ci.CrossOrigin ? SessionFlags.IsCrossOrigin : SessionFlags.None; } else { //Load/decode stored variables UserAgent = session.GetUserAgent(); SpecifiedOrigin = session.GetOriginUri(); - CrossOrigin = session.IsCrossOrigin(); SecurityProcol = session.GetSecurityProtocol(); + + flags |= session.IsCrossOrigin() ? SessionFlags.IsCrossOrigin : SessionFlags.None; } - CrossOriginMatch = ci.Origin != null && ci.Origin.Equals(SpecifiedOrigin); - IsSet = true; + + //Set cross origin orign match flags, if the stored origin, and connection origin + flags |= ci.Origin != null && ci.Origin.Equals(SpecifiedOrigin) ? SessionFlags.CrossOriginMatch : SessionFlags.None; + + //store flags + _flags = flags; } /// public bool Equals(SessionInfo other) => SessionID.Equals(other.SessionID, StringComparison.Ordinal); /// - public override bool Equals(object obj) => obj is SessionInfo si && Equals(si); + public override bool Equals(object? obj) => obj is SessionInfo si && Equals(si); /// public override int GetHashCode() => SessionID.GetHashCode(StringComparison.Ordinal); /// diff --git a/lib/Plugins.Runtime/src/IPluginEventListener.cs b/lib/Plugins.Runtime/src/IPluginEventListener.cs new file mode 100644 index 0000000..5d97343 --- /dev/null +++ b/lib/Plugins.Runtime/src/IPluginEventListener.cs @@ -0,0 +1,49 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Runtime +* File: IPluginEventListener.cs +* +* IPluginEventListener.cs is part of VNLib.Plugins.Runtime which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Runtime 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.Runtime 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.Runtime. If not, see http://www.gnu.org/licenses/. +*/ + +namespace VNLib.Plugins.Runtime +{ + /// + /// Represents a plugin event consumer. + /// + public interface IPluginEventListener + { + /// + /// Called by the registered + /// to notify this listener that the plugins within the collection + /// have been initialized and loaded + /// + /// The collection on which the load event occured + /// The registration state parameter + void OnPluginLoaded(PluginController controller, object? state); + /// + /// Called by the registered + /// to notify this listener that this plugins within the + /// collection have been unloaded + /// + /// The controller that is reloading + /// The registration state parameter + void OnPluginUnloaded(PluginController controller, object? state); + } +} diff --git a/lib/Plugins.Runtime/src/IPluginEventRegistrar.cs b/lib/Plugins.Runtime/src/IPluginEventRegistrar.cs new file mode 100644 index 0000000..120f437 --- /dev/null +++ b/lib/Plugins.Runtime/src/IPluginEventRegistrar.cs @@ -0,0 +1,47 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Runtime +* File: IPluginEventRegistrar.cs +* +* IPluginEventRegistrar.cs is part of VNLib.Plugins.Runtime which is +* part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Runtime 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.Runtime 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.Runtime. If not, see http://www.gnu.org/licenses/. +*/ + +namespace VNLib.Plugins.Runtime +{ + /// + /// Represents a type that accepts + /// event handlers and allow them to unload events + /// + public interface IPluginEventRegistrar + { + /// + /// Registers a plugin event listener + /// + /// The event handler instance to register + /// An optional state paremeter to pass to the event handler + void Register(IPluginEventListener listener, object? state = null); + + /// + /// Unregisters the event listener + /// + /// The event handler instance to unregister + /// A value that indicates if the event handler was successfully unregistered + bool Unregister(IPluginEventListener listener); + } +} diff --git a/lib/Plugins.Runtime/src/ITypedPluginConsumer.cs b/lib/Plugins.Runtime/src/ITypedPluginConsumer.cs new file mode 100644 index 0000000..9c8a477 --- /dev/null +++ b/lib/Plugins.Runtime/src/ITypedPluginConsumer.cs @@ -0,0 +1,47 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Runtime +* File: ITypedPluginConsumer.cs +* +* ITypedPluginConsumer.cs is part of VNLib.Plugins.Runtime which is +* part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Runtime 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.Runtime 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.Runtime. If not, see http://www.gnu.org/licenses/. +*/ + +namespace VNLib.Plugins.Runtime +{ + /// + /// An abstraction that represents a consumer of a dynamically loaded type. + /// + /// The service type to consume + public interface ITypedPluginConsumer + { + /// + /// Invoked when the instance of the desired type is loaded. + /// This is a new instance of the desired type + /// + /// A new instance of the requested type + void OnLoad(T plugin, object? state); + + /// + /// Called when the loader that maintains the instance is unloading + /// the type. + /// + /// The instance of the type that is being unloaded + void OnUnload(T plugin, object? state); + } +} diff --git a/lib/Plugins.Runtime/src/LivePlugin.cs b/lib/Plugins.Runtime/src/LivePlugin.cs index c0011dd..ae4c90b 100644 --- a/lib/Plugins.Runtime/src/LivePlugin.cs +++ b/lib/Plugins.Runtime/src/LivePlugin.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Runtime @@ -27,12 +27,12 @@ using System.Linq; using System.Reflection; using System.Text.Json; -using VNLib.Utils.Logging; using VNLib.Utils.Extensions; using VNLib.Plugins.Attributes; namespace VNLib.Plugins.Runtime { + /// /// /// Wrapper for a loaded instance, used internally @@ -46,6 +46,8 @@ namespace VNLib.Plugins.Runtime /// public class LivePlugin : IEquatable, IEquatable { + private bool _loaded; + /// /// The plugin's property during load time /// @@ -57,14 +59,24 @@ namespace VNLib.Plugins.Runtime /// by he current instance /// public IPlugin? Plugin { get; private set; } + + /// + /// The assembly that this plugin was created from + /// + public Assembly OriginAsm { get; } - private readonly Type PluginType; + /// + /// The exposed runtime type of the plugin. Equivalent to + /// calling Plugin.GetType() + /// + public Type PluginType { get; } private ConsoleEventHandlerSignature? PluginConsoleHandler; - internal LivePlugin(IPlugin plugin) + internal LivePlugin(IPlugin plugin, Assembly originAsm) { Plugin = plugin; + OriginAsm = originAsm; PluginType = plugin.GetType(); GetConsoleHandler(); } @@ -153,42 +165,38 @@ namespace VNLib.Plugins.Runtime /// /// Calls the method on the plugin if its loaded /// - internal void LoadPlugin() => Plugin?.Load(); + internal void LoadPlugin() + { + //Load and set loaded flag + Plugin?.Load(); + _loaded = true; + } /// - /// Unloads all loaded endpoints from - /// that they were loaded to, then unloads the plugin. + /// Unloads the plugin, only if the plugin was successfully loaded by + /// calling the event hook. /// - /// An optional log provider to write unload exceptions to - /// - /// If is no null unload exceptions are swallowed and written to the log - /// - internal void UnloadPlugin(ILogProvider? logSink) + internal void UnloadPlugin() { - /* - * We need to swallow plugin unload errors to avoid - * unknown state, making sure endpoints are properly - * unloaded! - */ + //Remove delegate handler to the plugin to remove refs + PluginConsoleHandler = null; + + //Only call unload if the plugin successfully loaded + if (!_loaded) + { + return; + } + try { - //Unload the plugin Plugin?.Unload(); } - catch (Exception ex) + finally { - //Create an unload wrapper for the exception - PluginUnloadException wrapper = new("Exception raised during plugin unload", ex); - if (logSink == null) - { - throw wrapper; - } - //Write error to log sink - logSink.Error(wrapper); + Plugin = null; } - Plugin = null; - PluginConsoleHandler = null; } + /// public override bool Equals(object? obj) { diff --git a/lib/Plugins.Runtime/src/LoaderExtensions.cs b/lib/Plugins.Runtime/src/LoaderExtensions.cs index 795dcf5..13fbb11 100644 --- a/lib/Plugins.Runtime/src/LoaderExtensions.cs +++ b/lib/Plugins.Runtime/src/LoaderExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Runtime @@ -23,98 +23,148 @@ */ using System; -using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + namespace VNLib.Plugins.Runtime { + /// + /// Contains extension methods for PluginLoader library + /// public static class LoaderExtensions { - /// - /// Searches all plugins within the current loader for a - /// single plugin that derrives the specified type - /// - /// The type the plugin must derrive from - /// - /// The instance of the plugin that derrives from the specified type - public static LivePlugin? GetExposedPlugin(this RuntimePluginLoader loader) + /* + * Class that manages a collection registration for a specific type + * dependency, and redirects the event calls for the consumed service + */ + private sealed class TypedRegistration : IPluginEventListener where T: class { - return loader.LivePlugins - .Where(static pl => typeof(T).IsAssignableFrom(pl.Plugin!.GetType())) - .SingleOrDefault(); + private readonly ITypedPluginConsumer _consumerEvents; + private readonly object? _userState; + + private T? _service; + private readonly Type _type; + + public TypedRegistration(ITypedPluginConsumer consumerEvents, Type type) + { + _consumerEvents = consumerEvents; + _type = type; + } + + + public void OnPluginLoaded(PluginController controller, object? state) + { + //Get the service from the loaded plugins + T service = controller.Plugins + .Where(pl => _type.IsAssignableFrom(pl.PluginType)) + .Select(static pl => (T)pl.Plugin!) + .First(); + + //Call load with the exported type + _consumerEvents.OnLoad(service, _userState); + + //Store for unload + _service = service; + } + + public void OnPluginUnloaded(PluginController controller, object? state) + { + //Unload + _consumerEvents.OnUnload(_service!, _userState); + _service = null; + } } /// - /// Searches all plugins within the current loader for a - /// single plugin that derrives the specified type + /// Registers a plugin even handler for the current + /// for a specific type. /// - /// The type the plugin must derrive from - /// - /// The instance of your custom type casted, or null if not found or could not be casted - public static T? GetExposedTypeFromPlugin(this RuntimePluginLoader loader) where T: class + /// + /// + /// The typed plugin instance event consumer + /// A handle that manages this event registration + /// + public static PluginEventRegistration RegisterForType(this PluginController collection, ITypedPluginConsumer consumer) where T: class { - LivePlugin? plugin = loader.LivePlugins - .Where(static pl => typeof(T).IsAssignableFrom(pl.Plugin!.GetType())) - .SingleOrDefault(); + Type serviceType = typeof(T); - return plugin?.Plugin as T; + //Confim the type is exposed by this collection + if(!ExposesType(collection, serviceType)) + { + throw new ArgumentException("The requested type is not exposed in this assembly"); + } + + //Create new typed listener + TypedRegistration reg = new(consumer, serviceType); + + //register event handler + return Register(collection, reg, null); } /// - /// Registers a listener delegate method to invoke when the - /// current is reloaded, and passes - /// the new instance of the specified type + /// Registers a handler to listen for plugin load/unload events /// - /// The single plugin type to register a listener for - /// - /// The delegate method to invoke when the loader has reloaded plugins /// - public static bool RegisterListenerForSingle(this RuntimePluginLoader loader, Action reloaded) where T: class + /// A handle that will unregister the listener when disposed + public static PluginEventRegistration Register(this IPluginEventRegistrar reg, IPluginEventListener listener, object? state = null) { - _ = reloaded ?? throw new ArgumentNullException(nameof(reloaded)); + reg.Register(listener, state); + return new(reg, listener); + } - //try to get the casted type from the loader - T? current = loader.GetExposedTypeFromPlugin(); + /// + /// Loads the configuration file into its format + /// for reading. + /// + /// + /// A new of the loaded configuration file + public static async Task GetPluginConfigAsync(this RuntimePluginLoader loader) + { + //Open and read the config file + await using FileStream confStream = File.OpenRead(loader.PluginConfigPath); - if (current == null) - { - return false; - } - else + JsonDocumentOptions jdo = new() { - loader.Reloaded += delegate (object? sender, EventArgs args) - { - RuntimePluginLoader wpl = (sender as RuntimePluginLoader)!; - //Get the new loaded type - T newT = (wpl.GetExposedPlugin()!.Plugin as T)!; - //Invoke reloaded action - reloaded(current, newT); - //update the new current instance - current = newT; - }; - - return true; - } + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip, + }; + + //parse the plugin config file + return await JsonDocument.ParseAsync(confStream, jdo); } /// - /// Gets all endpoints exposed by all exported plugin instances - /// within the current loader + /// Determines if the current + /// exposes the desired type on is + /// type. /// - /// - /// An enumeration of all endpoints - public static IEnumerable GetEndpoints(this RuntimePluginLoader loader) => loader.LivePlugins.SelectMany(static pl => pl.Plugin!.GetEndpoints()); + /// + /// The desired type to request + /// True if the plugin exposes the desired type, false otherwise + public static bool ExposesType(this PluginController collection, Type type) + { + return collection.Plugins + .Where(pl => type.IsAssignableFrom(pl.PluginType)) + .Any(); + } /// - /// Determines if any loaded plugin types exposes an instance of the - /// specified type + /// Searches all plugins within the current loader for a + /// single plugin that derrives the specified type /// - /// + /// The type the plugin must derrive from /// - /// True if any plugin instance exposes a the specified type, false otherwise - public static bool ExposesType(this RuntimePluginLoader loader) where T : class + /// The instance of your custom type casted, or null if not found or could not be casted + public static T? GetExposedTypes(this PluginController collection) where T: class { - return loader.LivePlugins.Any(static pl => typeof(T).IsAssignableFrom(pl.Plugin?.GetType())); + LivePlugin? plugin = collection.Plugins + .Where(static pl => typeof(T).IsAssignableFrom(pl.PluginType)) + .SingleOrDefault(); + + return plugin?.Plugin as T; } } } diff --git a/lib/Plugins.Runtime/src/PluginController.cs b/lib/Plugins.Runtime/src/PluginController.cs new file mode 100644 index 0000000..14ea7f0 --- /dev/null +++ b/lib/Plugins.Runtime/src/PluginController.cs @@ -0,0 +1,127 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Runtime +* File: PluginController.cs +* +* PluginController.cs is part of VNLib.Plugins.Runtime which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Runtime 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.Runtime 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.Runtime. 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.Runtime +{ + /// + /// Manages the lifetime of a collection of instances, + /// and their dependent event listeners + /// + public sealed class PluginController : IPluginEventRegistrar + { + private readonly List _plugins; + private readonly List> _listeners; + + internal PluginController() + { + _plugins = new (); + _listeners = new (); + } + + /// + /// The current collection of plugins. Valid before the unload event. + /// + public IEnumerable Plugins => _plugins; + + /// + /// + public void Register(IPluginEventListener listener, object? state = null) + { + _ = listener ?? throw new ArgumentNullException(nameof(listener)); + + _listeners.Add(new(listener, state)); + } + + /// + public bool Unregister(IPluginEventListener listener) + { + //Remove listener + return _listeners.RemoveAll(p => p.Key == listener) > 0; + } + + + internal void InitializePlugins(Assembly asm) + { + //get all Iplugin types + Type[] types = asm.GetTypes().Where(static type => !type.IsAbstract && typeof(IPlugin).IsAssignableFrom(type)).ToArray(); + + //Initialize the new plugin instances + IPlugin[] plugins = types.Select(static t => (IPlugin)Activator.CreateInstance(t)!).ToArray(); + + //Crate new containers + LivePlugin[] lps = plugins.Select(p => new LivePlugin(p, asm)).ToArray(); + + //Store containers + _plugins.AddRange(lps); + } + + internal void ConfigurePlugins(JsonDocument hostDom, JsonDocument pluginDom, string[] cliArgs) + { + _plugins.TryForeach(lp => lp.InitConfig(hostDom, pluginDom)); + _plugins.TryForeach(lp => lp.InitLog(cliArgs)); + } + + internal void LoadPlugins() + { + //Load all plugins + _plugins.TryForeach(static p => p.LoadPlugin()); + + //Notify event handlers + _listeners.TryForeach(l => l.Key.OnPluginLoaded(this, l.Value)); + } + + internal void UnloadPlugins() + { + try + { + //Notify event handlers + _listeners.TryForeach(l => l.Key.OnPluginUnloaded(this, l.Value)); + + //Unload plugin instances + _plugins.TryForeach(static p => p.UnloadPlugin()); + } + finally + { + //Always + _plugins.Clear(); + } + } + + internal void Dispose() + { + _plugins.Clear(); + _listeners.Clear(); + } + + + } +} diff --git a/lib/Plugins.Runtime/src/PluginEventRegistration.cs b/lib/Plugins.Runtime/src/PluginEventRegistration.cs new file mode 100644 index 0000000..37d6162 --- /dev/null +++ b/lib/Plugins.Runtime/src/PluginEventRegistration.cs @@ -0,0 +1,69 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Runtime +* File: PluginEventRegistration.cs +* +* PluginEventRegistration.cs is part of VNLib.Plugins.Runtime which is +* part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Runtime 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.Runtime 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.Runtime. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Plugins.Runtime +{ + /// + /// Holds a registration for events to a given . + /// The event listener is unregistered from events when this registration is disposed. + /// + public readonly record struct PluginEventRegistration : IDisposable + { + private readonly IPluginEventRegistrar _registrar; + private readonly IPluginEventListener _listener; + + internal PluginEventRegistration(IPluginEventRegistrar container, IPluginEventListener listener) + { + _listener = listener; + _registrar = container; + } + + /// + /// Unreigsers the listner and releases held resources + /// + public readonly void Dispose() + { + _ = _registrar?.Unregister(_listener); + } + + /// + /// Unregisters a previously registered + /// from the it was registered to + /// + /// + public readonly void Unregister() + { + if (_registrar == null) + { + return; + } + if (!_registrar.Unregister(_listener)) + { + throw new InvalidOperationException("The listner has already been unregistered"); + } + } + } +} diff --git a/lib/Plugins.Runtime/src/RuntimePluginLoader.cs b/lib/Plugins.Runtime/src/RuntimePluginLoader.cs index c688f8b..d79def3 100644 --- a/lib/Plugins.Runtime/src/RuntimePluginLoader.cs +++ b/lib/Plugins.Runtime/src/RuntimePluginLoader.cs @@ -1,11 +1,11 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Runtime -* File: DynamicPluginLoader.cs +* File: RuntimePluginLoader.cs * -* DynamicPluginLoader.cs is part of VNLib.Plugins.Runtime which is part of the larger +* RuntimePluginLoader.cs is part of VNLib.Plugins.Runtime which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Plugins.Runtime is free software: you can redistribute it and/or modify @@ -24,20 +24,16 @@ using System; using System.IO; -using System.Linq; using System.Text.Json; using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; -using System.Collections.Generic; using McMaster.NETCore.Plugins; using VNLib.Utils; using VNLib.Utils.IO; using VNLib.Utils.Logging; -using VNLib.Utils.Extensions; - namespace VNLib.Plugins.Runtime { @@ -45,50 +41,23 @@ namespace VNLib.Plugins.Runtime /// A runtime .NET assembly loader specialized to load /// assemblies that export types. /// - public class RuntimePluginLoader : VnDisposeable + public sealed class RuntimePluginLoader : VnDisposeable { - protected readonly PluginLoader Loader; - protected readonly string PluginPath; - protected readonly JsonDocument HostConfig; - protected readonly ILogProvider? Log; - protected readonly LinkedList LoadedPlugins; - - /// - /// A readonly collection of all loaded plugin wrappers - /// - public IReadOnlyCollection LivePlugins => LoadedPlugins; - - /// - /// An event that is raised before the loader - /// unloads all plugin instances - /// - protected event EventHandler? OnBeforeReloaded; - /// - /// An event that is raised after a successfull reload of all new - /// plugins for the instance - /// - protected event EventHandler? OnAfterReloaded; - - /// - /// Raised when the current loader has reloaded the assembly and - /// all plugins were successfully loaded. - /// - public event EventHandler? Reloaded; + private readonly PluginLoader Loader; + private readonly string PluginPath; + private readonly JsonDocument HostConfig; + private readonly ILogProvider? Log; /// - /// The current plugin's JSON configuration DOM loaded from the plugin's directory - /// if it exists. Only valid after first initalization + /// Gets the plugin lifetime manager. /// - public JsonDocument? PluginConfigDOM { get; private set; } - /// - /// Optional loader arguments object for the plugin - /// - protected JsonElement? LoaderArgs { get; private set; } + public PluginController Controller { get; } /// /// The path of the plugin's configuration file. (Default = pluginPath.json) /// - public string PluginConfigPath { get; init; } + public string PluginConfigPath { get; } + /// /// Creates a new with the specified /// assembly location and host config. @@ -98,18 +67,16 @@ namespace VNLib.Plugins.Runtime /// The configuration DOM to merge with plugin config DOM and pass to enabled plugins /// A value that specifies if the assembly can be unloaded /// A value that spcifies if the loader will listen for changes to the assembly file and reload the plugins - /// A value that specifies if assembly dependencies are loaded on-demand /// /// The argument may be null if is false /// /// - public RuntimePluginLoader(string pluginPath, JsonDocument? hostConfig = null, ILogProvider? log = null, bool unloadable = false, bool hotReload = false, bool lazy = false) + public RuntimePluginLoader(string pluginPath, JsonDocument? hostConfig = null, ILogProvider? log = null, bool unloadable = false, bool hotReload = false) :this( new PluginConfig(pluginPath) { IsUnloadable = unloadable || hotReload, EnableHotReload = hotReload, - IsLazyLoaded = lazy, ReloadDelay = TimeSpan.FromSeconds(1), PreferSharedTypes = true, DefaultContext = AssemblyLoadContext.Default @@ -117,6 +84,7 @@ namespace VNLib.Plugins.Runtime hostConfig, log) { } + /// /// Creates a new with the specified config and host config dom. /// @@ -126,124 +94,119 @@ namespace VNLib.Plugins.Runtime /// public RuntimePluginLoader(PluginConfig config, JsonDocument? hostConfig, ILogProvider? log) { - //Add the assembly from which the IPlugin library was loaded from - config.SharedAssemblies.Add(typeof(IPlugin).Assembly.GetName()); - + //Shared types is required so the default load context shares types + config.PreferSharedTypes = true; + //Default to empty config if null HostConfig = hostConfig ?? JsonDocument.Parse("{}"); + Loader = new(config); PluginPath = config.MainAssemblyPath; Log = log; - Loader.Reloaded += Loader_Reloaded; + + //Only regiser reload handler if the load context is unloadable + if (config.IsUnloadable) + { + //Init reloaded event handler + Loader.Reloaded += Loader_Reloaded; + } + //Set the config path default PluginConfigPath = Path.ChangeExtension(PluginPath, ".json"); - LoadedPlugins = new(); + + //Init container + Controller = new(); } private async void Loader_Reloaded(object sender, PluginReloadedEventArgs eventArgs) { try { - //Invoke reloaded events - OnBeforeReloaded?.Invoke(this, eventArgs); - //Unload all endpoints - LoadedPlugins.TryForeach(lp => lp.UnloadPlugin(Log)); - //Clear list of loaded plugins - LoadedPlugins.Clear(); - //Unload the plugin config - PluginConfigDOM?.Dispose(); + //All plugins must be unloaded forst + UnloadAll(); + //Reload the assembly and - await InitLoaderAsync(); - //fire after loaded - OnAfterReloaded?.Invoke(this, eventArgs); - //Raise the external reloaded event - Reloaded?.Invoke(this, EventArgs.Empty); + await InitializeController(); + + //Load plugins + LoadPlugins(); } catch (Exception ex) { - Log?.Error(ex); + Log?.Error("Failed reload plugins for {loader}\n{ex}", PluginPath, ex); } } /// - /// Initializes the plugin loader, the assembly, and all public - /// types + /// Initializes the plugin loader, and populates the + /// with initialized plugins. /// /// A task that represents the initialization - public async Task InitLoaderAsync() + /// + /// + public async Task InitializeController() { - //Load the main assembly - Assembly PluginAsm = Loader.LoadDefaultAssembly(); - //Get the plugin's configuration file - if (FileOperations.FileExists(PluginConfigPath)) + JsonDocument? pluginConfig = null; + + try { - //Open and read the config file - await using FileStream confStream = File.OpenRead(PluginConfigPath); - JsonDocumentOptions jdo = new() + //Get the plugin's configuration file + if (FileOperations.FileExists(PluginConfigPath)) { - AllowTrailingCommas = true, - CommentHandling = JsonCommentHandling.Skip, - }; - //parse the plugin config file - PluginConfigDOM = await JsonDocument.ParseAsync(confStream, jdo); - //Store the config loader args - if (PluginConfigDOM.RootElement.TryGetProperty("loader_args", out JsonElement loaderEl)) + pluginConfig = await this.GetPluginConfigAsync(); + } + else { - LoaderArgs = loaderEl; + //Set plugin config dom to an empty object if the file does not exist + pluginConfig = JsonDocument.Parse("{}"); } + + //Load the main assembly + Assembly PluginAsm = Loader.LoadDefaultAssembly(); + + //Init container from the assembly + Controller.InitializePlugins(PluginAsm); + + string[] cliArgs = Environment.GetCommandLineArgs(); + + //Configure log/doms + Controller.ConfigurePlugins(HostConfig, pluginConfig, cliArgs); } - else - { - //Set plugin config dom to an empty object if the file does not exist - PluginConfigDOM = JsonDocument.Parse("{}"); - LoaderArgs = null; - } - - string[] cliArgs = Environment.GetCommandLineArgs(); - - //Get all types that implement the IPlugin interface - IEnumerable plugins = PluginAsm.GetTypes().Where(static type => !type.IsAbstract && typeof(IPlugin).IsAssignableFrom(type)) - //Create the plugin instances - .Select(static type => (Activator.CreateInstance(type) as IPlugin)!); - //Load all plugins that implement the Iplugin interface - foreach (IPlugin plugin in plugins) + finally { - //Load wrapper - LivePlugin lp = new(plugin); - try - { - //Init config - lp.InitConfig(HostConfig, PluginConfigDOM); - //Init log handler - lp.InitLog(cliArgs); - //Load the plugin - lp.LoadPlugin(); - //Create new plugin loader for the plugin - LoadedPlugins.AddLast(lp); - } - catch (TargetInvocationException te) when (te.InnerException is not null) - { - throw te.InnerException; - } + pluginConfig?.Dispose(); } } + + /// + /// Loads all configured plugins by calling + /// event hook on the current thread. Loading exceptions are aggregated so not + /// to block individual loading. + /// + /// + public void LoadPlugins() + { + //Load all plugins + Controller.LoadPlugins(); + } + /// /// Manually reload the internal - /// which will reload the assembly and its plugins and endpoints + /// which will reload the assembly and its plugins /// - public void ReloadPlugin() => Loader.Reload(); + public void ReloadPlugins() => Loader.Reload(); /// /// Attempts to unload all plugins. /// /// - public void UnloadAll() => LoadedPlugins.TryForeach(lp => lp.UnloadPlugin(Log)); + public void UnloadAll() => Controller.UnloadPlugins(); /// protected override void Free() { + Controller.Dispose(); Loader.Dispose(); - PluginConfigDOM?.Dispose(); } } diff --git a/lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj b/lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj index 37a9e74..87dbcfe 100644 --- a/lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj +++ b/lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj @@ -5,7 +5,6 @@ net6.0 VNLib.Plugins.Runtime VNLib.Plugins.Runtime - 1.0.1.1 latest-all false True @@ -44,7 +43,7 @@ - + diff --git a/lib/Utils/src/Extensions/JsonExtensions.cs b/lib/Utils/src/Extensions/JsonExtensions.cs index 523f772..55ad958 100644 --- a/lib/Utils/src/Extensions/JsonExtensions.cs +++ b/lib/Utils/src/Extensions/JsonExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Utils @@ -63,19 +63,6 @@ namespace VNLib.Utils.Extensions public static class JsonExtensions { - /// - /// Converts a JSON encoded string to an object of the specified type - /// - /// Output type of the object - /// - /// to use during de-serialization - /// The new object or default if the string is null or empty - /// - /// - public static T? AsJsonObject(this string value, JsonSerializerOptions? options = null) - { - return !string.IsNullOrWhiteSpace(value) ? JsonSerializer.Deserialize(value, options) : default; - } /// /// Converts a JSON encoded binary data to an object of the specified type /// @@ -158,18 +145,6 @@ namespace VNLib.Utils.Extensions return conf.TryGetValue(propertyName, out JsonElement el) ? el.GetString() : null; } - /// - /// Attemts to serialze an object to a JSON encoded string - /// - /// - /// to use during serialization - /// A JSON encoded string of the serialized object, or null if the object is null - /// - public static string? ToJsonString(this T obj, JsonSerializerOptions? options = null) - { - return obj == null ? null : JsonSerializer.Serialize(obj, options); - } - /// /// Merges the current with another to /// create a new document of combined properties diff --git a/lib/Utils/src/VnEncoding.cs b/lib/Utils/src/VnEncoding.cs index 89863aa..c9cdbb0 100644 --- a/lib/Utils/src/VnEncoding.cs +++ b/lib/Utils/src/VnEncoding.cs @@ -74,67 +74,7 @@ namespace VNLib.Utils throw; } } - - /// - /// Attempts to deserialze a json object from a stream of UTF8 data - /// - /// The type of the object to deserialize - /// Binary data to read from - /// object to pass to deserializer - /// The object decoded from the stream - /// - /// - public static T? JSONDeserializeFromBinary(Stream? data, JsonSerializerOptions? options = null) - { - //Return default if null - if (data == null) - { - return default; - } - //Create a memory stream as a buffer - using VnMemoryStream ms = new(); - //Copy stream data to memory - data.CopyTo(ms, null); - if (ms.Length > 0) - { - //Rewind - ms.Position = 0; - //Recover data from stream - return ms.AsSpan().AsJsonObject(options); - } - //Stream is empty - return default; - } - /// - /// Attempts to deserialze a json object from a stream of UTF8 data - /// - /// Binary data to read from - /// - /// object to pass to deserializer - /// The object decoded from the stream - /// - /// - public static object? JSONDeserializeFromBinary(Stream? data, Type type, JsonSerializerOptions? options = null) - { - //Return default if null - if (data == null) - { - return default; - } - //Create a memory stream as a buffer - using VnMemoryStream ms = new(); - //Copy stream data to memory - data.CopyTo(ms, null); - if (ms.Length > 0) - { - //Rewind - ms.Position = 0; - //Recover data from stream - return JsonSerializer.Deserialize(ms.AsSpan(), type, options); - } - //Stream is empty - return default; - } + /// /// Attempts to deserialze a json object from a stream of UTF8 data /// @@ -315,7 +255,7 @@ namespace VNLib.Utils //Fill remaining bytes with padding chars for(; rounds < 8; rounds++) { - //Append trailing '=' + //Append trailing '=' padding character writer.Append('='); } } -- cgit