aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-03-09 01:48:28 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-03-09 01:48:28 -0500
commit5ddef0fcb742e77b99a0e17015d2eea0a1d4131a (patch)
treec1c88284b11b70d9f373215d8d54e8a168cc5700
parentdab71d5597fdfbe71f6ac310a240835716e952a5 (diff)
Omega cache, session, and account provider complete overhaul
-rw-r--r--lib/Hashing.Portable/src/Argon2/VnArgon2.cs23
-rw-r--r--lib/Hashing.Portable/src/IdentityUtility/IJsonWebKey.cs50
-rw-r--r--lib/Hashing.Portable/src/IdentityUtility/JsonWebKey.cs193
-rw-r--r--lib/Hashing.Portable/src/IdentityUtility/ReadOnlyJsonWebKey.cs39
-rw-r--r--lib/Net.Http/src/Core/ConnectionInfo.cs2
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs33
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs6
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs28
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/IPluginWrapper.cs54
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs32
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/IUnloadableServiceProvider.cs42
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs202
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs62
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs240
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs284
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs76
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/VNLib.Plugins.Essentials.ServiceStack.csproj1
-rw-r--r--lib/Plugins.Essentials/src/Accounts/AccountUtils.cs807
-rw-r--r--lib/Plugins.Essentials/src/Accounts/AuthorzationCheckLevel.cs56
-rw-r--r--lib/Plugins.Essentials/src/Accounts/ClientSecurityToken.cs43
-rw-r--r--lib/Plugins.Essentials/src/Accounts/IAccountSecurityProvider.cs90
-rw-r--r--lib/Plugins.Essentials/src/Accounts/IClientAuthorization.cs45
-rw-r--r--lib/Plugins.Essentials/src/Accounts/IClientSecInfo.cs46
-rw-r--r--lib/Plugins.Essentials/src/Accounts/IPasswordHashingProvider.cs80
-rw-r--r--lib/Plugins.Essentials/src/Accounts/LoginMessage.cs12
-rw-r--r--lib/Plugins.Essentials/src/Accounts/PasswordChallengeResult.cs38
-rw-r--r--lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs157
-rw-r--r--lib/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs14
-rw-r--r--lib/Plugins.Essentials/src/EventProcessor.cs35
-rw-r--r--lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs12
-rw-r--r--lib/Plugins.Essentials/src/Extensions/HttpCookie.cs26
-rw-r--r--lib/Plugins.Essentials/src/HttpEntity.cs4
-rw-r--r--lib/Plugins.Essentials/src/IEpProcessingOptions.cs8
-rw-r--r--lib/Plugins.Essentials/src/IWebProcessorInfo.cs82
-rw-r--r--lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs137
-rw-r--r--lib/Plugins.Essentials/src/Sessions/SessionBase.cs20
-rw-r--r--lib/Plugins.Essentials/src/Sessions/SessionInfo.cs165
-rw-r--r--lib/Plugins.Runtime/src/IPluginEventListener.cs49
-rw-r--r--lib/Plugins.Runtime/src/IPluginEventRegistrar.cs47
-rw-r--r--lib/Plugins.Runtime/src/ITypedPluginConsumer.cs47
-rw-r--r--lib/Plugins.Runtime/src/LivePlugin.cs66
-rw-r--r--lib/Plugins.Runtime/src/LoaderExtensions.cs174
-rw-r--r--lib/Plugins.Runtime/src/PluginController.cs127
-rw-r--r--lib/Plugins.Runtime/src/PluginEventRegistration.cs69
-rw-r--r--lib/Plugins.Runtime/src/RuntimePluginLoader.cs201
-rw-r--r--lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj3
-rw-r--r--lib/Utils/src/Extensions/JsonExtensions.cs27
-rw-r--r--lib/Utils/src/VnEncoding.cs64
48 files changed, 2513 insertions, 1605 deletions
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<byte> buffer = PwHeap.Alloc<byte>(saltbytes + passBytes, true);
+ using IMemoryHandle<byte> buffer = PwHeap.Alloc<byte>(saltbytes + passBytes, true);
Span<byte> saltBuffer = buffer.AsSpan(0, saltbytes);
Span<byte> passBuffer = buffer.AsSpan(passBytes);
@@ -178,10 +179,10 @@ namespace VNLib.Hashing
int passBytes = LocEncoding.GetByteCount(password);
//Alloc memory for password
- using MemoryHandle<byte> pwdHandle = PwHeap.Alloc<byte>(passBytes, true);
+ using IMemoryHandle<byte> pwdHandle = PwHeap.Alloc<byte>(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<byte> hashHandle = PwHeap.Alloc<byte>(hashLen, true);
+ using IMemoryHandle<byte> hashHandle = PwHeap.Alloc<byte>(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}";
}
+
/// <summary>
/// Exposes the raw Argon2-ID hashing api to C#, using spans (pins memory references)
@@ -231,7 +233,7 @@ namespace VNLib.Hashing
/// <param name="parallelism">Degree of parallelism</param>
/// <param name="timeCost">Time cost of operation</param>
/// <exception cref="VnArgon2Exception"></exception>
- public static void Hash2id(ReadOnlySpan<byte> password, ReadOnlySpan<byte> salt, ReadOnlySpan<byte> secret, in Span<byte> rawHashOutput,
+ public static void Hash2id(ReadOnlySpan<byte> password, ReadOnlySpan<byte> salt, ReadOnlySpan<byte> secret, Span<byte> 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<byte> outputHandle = PwHeap.Alloc<byte>(hashBytes.Length, true);
+ using IMemoryHandle<byte> outputHandle = PwHeap.Alloc<byte>(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<byte> rawBufferHandle = MemoryUtil.Shared.Alloc<byte>(passBase64BufSize + saltBase64BufSize + rawPassLen, true);
+ using IMemoryHandle<byte> rawBufferHandle = MemoryUtil.Shared.Alloc<byte>(passBase64BufSize + saltBase64BufSize + rawPassLen, true);
//Split buffers
Span<byte> 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
+{
+ /// <summary>
+ /// An abstraction for basic JsonWebKey operations
+ /// </summary>
+ public interface IJsonWebKey
+ {
+ /// <summary>
+ /// The key usage, may be Siganture, or Encryption
+ /// </summary>
+ JwkKeyUsage KeyUse { get; }
+
+ /// <summary>
+ /// The cryptographic algorithm this key is to be used for
+ /// </summary>
+ string Algorithm { get; }
+
+ /// <summary>
+ /// Gets miscelaneous key properties on demand. May return null results if the property
+ /// is not defined in the current key
+ /// </summary>
+ /// <param name="propertyName">The name of the key property to get</param>
+ /// <returns>The value at the key property</returns>
+ 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
{}
}
+ /// <summary>
+ /// The JWK key usage flags
+ /// </summary>
+ public enum JwkKeyUsage
+ {
+ /// <summary>
+ /// Default/not supported operation
+ /// </summary>
+ None,
+ /// <summary>
+ /// The key supports cryptographic signatures
+ /// </summary>
+ Signature,
+ /// <summary>
+ /// The key supports encryption operations
+ /// </summary>
+ Encryption
+ }
+
+ /// <summary>
+ /// Contains extension methods for verifying and signing <see cref="JsonWebToken"/>
+ /// using <see cref="IJsonWebKey"/>s.
+ /// </summary>
public static class JsonWebKey
- {
-
+ {
+
/// <summary>
/// Verifies the <see cref="JsonWebToken"/> against the supplied
/// Json Web Key in <see cref="JsonDocument"/> format
@@ -70,81 +93,73 @@ namespace VNLib.Hashing.IdentityUtility
/// <exception cref="FormatException"></exception>
/// <exception cref="OutOfMemoryException"></exception>
/// <exception cref="EncryptionTypeNotSupportedException"></exception>
- 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<TKey>(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
}
}
-
- /// <summary>
- /// Verifies the <see cref="JsonWebToken"/> against the supplied
- /// <see cref="ReadOnlyJsonWebKey"/>
- /// </summary>
- /// <param name="token"></param>
- /// <param name="jwk">The supplied single Json Web Key</param>
- /// <returns>True if required JWK data exists, ciphers were created, and data is verified, false otherwise</returns>
- /// <exception cref="FormatException"></exception>
- /// <exception cref="OutOfMemoryException"></exception>
- /// <exception cref="EncryptionTypeNotSupportedException"></exception>
- public static bool VerifyFromJwk(this JsonWebToken token, ReadOnlyJsonWebKey jwk) => token.VerifyFromJwk(jwk.KeyElement);
/// <summary>
/// Signs the <see cref="JsonWebToken"/> with the supplied JWK json element
@@ -173,66 +176,63 @@ namespace VNLib.Hashing.IdentityUtility
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="EncryptionTypeNotSupportedException"></exception>
- public static void SignFromJwk(this JsonWebToken token, in JsonElement jwk)
+ public static void SignFromJwk<T>(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;
@@ -265,35 +265,11 @@ namespace VNLib.Hashing.IdentityUtility
}
/// <summary>
- /// Signs the <see cref="JsonWebToken"/> with the supplied JWK json element
- /// </summary>
- /// <param name="token"></param>
- /// <param name="jwk">The JWK in the <see cref="ReadOnlyJsonWebKey"/> </param>
- /// <exception cref="ArgumentNullException"></exception>
- /// <exception cref="InvalidOperationException"></exception>
- /// <exception cref="EncryptionTypeNotSupportedException"></exception>
- public static void SignFromJwk(this JsonWebToken token, ReadOnlyJsonWebKey jwk) => token.SignFromJwk(jwk.KeyElement);
-
- /// <summary>
- /// Gets the <see cref="RSA"/> public key algorithm for the current <see cref="ReadOnlyJsonWebKey"/>
- /// </summary>
- /// <param name="key"></param>
- /// <returns>The <see cref="RSA"/> algorithm of the public key if loaded</returns>
- public static RSA? GetRSAPublicKey(this ReadOnlyJsonWebKey key) => key == null ? null : GetRSAPublicKey(key.KeyElement);
-
- /// <summary>
- /// Gets the <see cref="RSA"/> private key algorithm for the current <see cref="ReadOnlyJsonWebKey"/>
- /// </summary>
- /// <param name="key"></param>
- ///<returns>The <see cref="RSA"/> algorithm of the private key key if loaded</returns>
- public static RSA? GetRSAPrivateKey(this ReadOnlyJsonWebKey key) => key == null ? null : GetRSAPrivateKey(key.KeyElement);
-
- /// <summary>
/// Gets the RSA public key algorithm from the supplied Json Web Key <see cref="JsonElement"/>
/// </summary>
/// <param name="jwk">The element that contains the JWK data</param>
/// <returns>The <see cref="RSA"/> algorithm if found, or null if the element does not contain public key</returns>
- public static RSA? GetRSAPublicKey(in JsonElement jwk)
+ public static RSA? GetRSAPublicKey<TKey>(this TKey jwk) where TKey: IJsonWebKey
{
RSAParameters? rSAParameters = GetRsaParameters(in jwk, false);
//Create rsa from params
@@ -305,18 +281,18 @@ namespace VNLib.Hashing.IdentityUtility
/// </summary>
/// <param name="jwk"></param>
/// <returns>The <see cref="RSA"/> algorithm if found, or null if the element does not contain private key</returns>
- public static RSA? GetRSAPrivateKey(in JsonElement jwk)
+ public static RSA? GetRSAPrivateKey<TKey>(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<TKey>(in TKey jwk, bool includePrivateKey) where TKey : IJsonWebKey
{
//Get the RSA public key credentials
- ReadOnlySpan<char> e = jwk.GetPropString("e");
- ReadOnlySpan<char> n = jwk.GetPropString("n");
+ ReadOnlySpan<char> e = jwk.GetKeyProperty("e");
+ ReadOnlySpan<char> 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<char> d = jwk.GetPropString("d");
- ReadOnlySpan<char> dp = jwk.GetPropString("dq");
- ReadOnlySpan<char> dq = jwk.GetPropString("dp");
- ReadOnlySpan<char> p = jwk.GetPropString("p");
- ReadOnlySpan<char> q = jwk.GetPropString("q");
+ ReadOnlySpan<char> d = jwk.GetKeyProperty("d");
+ ReadOnlySpan<char> dp = jwk.GetKeyProperty("dq");
+ ReadOnlySpan<char> dq = jwk.GetKeyProperty("dp");
+ ReadOnlySpan<char> p = jwk.GetKeyProperty("p");
+ ReadOnlySpan<char> 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),
};
}
-
}
-
-
- /// <summary>
- /// Gets the ECDsa public key algorithm for the current <see cref="ReadOnlyJsonWebKey"/>
- /// </summary>
- /// <param name="key"></param>
- /// <returns>The <see cref="ECDsa"/> algorithm of the public key if loaded</returns>
- public static ECDsa? GetECDsaPublicKey(this ReadOnlyJsonWebKey key) => key == null ? null : GetECDsaPublicKey(key.KeyElement);
-
- /// <summary>
- /// Gets the <see cref="ECDsa"/> private key algorithm for the current <see cref="ReadOnlyJsonWebKey"/>
- /// </summary>
- /// <param name="key"></param>
- ///<returns>The <see cref="ECDsa"/> algorithm of the private key key if loaded</returns>
- public static ECDsa? GetECDsaPrivateKey(this ReadOnlyJsonWebKey key) => key == null ? null : GetECDsaPrivateKey(key.KeyElement);
+
/// <summary>
/// Gets the ECDsa public key algorithm from the supplied Json Web Key <see cref="JsonElement"/>
/// </summary>
/// <param name="jwk">The public key element</param>
/// <returns>The <see cref="ECDsa"/> algorithm from the key if loaded, null if no key data was found</returns>
- public static ECDsa? GetECDsaPublicKey(in JsonElement jwk)
+ public static ECDsa? GetECDsaPublicKey<TKey>(this TKey jwk) where TKey: IJsonWebKey
{
//Get the EC params
ECParameters? ecParams = GetECParameters(in jwk, false);
@@ -389,7 +350,7 @@ namespace VNLib.Hashing.IdentityUtility
/// </summary>
/// <param name="jwk">The element that contains the private key data</param>
/// <returns>The <see cref="ECDsa"/> algorithm from the key if loaded, null if no key data was found</returns>
- public static ECDsa? GetECDsaPrivateKey(in JsonElement jwk)
+ public static ECDsa? GetECDsaPrivateKey<TKey>(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<TKey>(in TKey jwk, bool includePrivate) where TKey : IJsonWebKey
{
//Get the RSA public key credentials
- ReadOnlySpan<char> x = jwk.GetPropString("x");
- ReadOnlySpan<char> y = jwk.GetPropString("y");
+ ReadOnlySpan<char> x = jwk.GetKeyProperty("x");
+ ReadOnlySpan<char> y = jwk.GetKeyProperty("y");
//Optional private key
- ReadOnlySpan<char> d = includePrivate ? jwk.GetPropString("d") : null;
+ ReadOnlySpan<char> 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.
/// </summary>
- public sealed class ReadOnlyJsonWebKey : VnDisposeable
+ public sealed class ReadOnlyJsonWebKey : VnDisposeable, IJsonWebKey
{
private readonly JsonElement _jwk;
- private readonly JsonDocument? doc;
+ private readonly JsonDocument? _doc;
/// <summary>
/// Creates a new instance of <see cref="ReadOnlyJsonWebKey"/> from a <see cref="JsonElement"/>.
@@ -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,
+ };
}
/// <summary>
@@ -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,
+ };
}
/// <summary>
@@ -112,16 +128,17 @@ namespace VNLib.Hashing.IdentityUtility
/// Returns the JWT header that matches this key
/// </summary>
public IReadOnlyDictionary<string, string?> JwtHeader { get; }
+
+ ///<inheritdoc/>
+ public JwkKeyUsage KeyUse { get; }
- /// <summary>
- /// The key element
- /// </summary>
- internal JsonElement KeyElement => _jwk;
+ ///<inheritdoc/>
+ public string? GetKeyProperty(string propertyName) => _jwk.GetPropString(propertyName);
///<inheritdoc/>
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;
}
+
/// <summary>
/// Determines if the connection accepts any content type
/// </summary>
@@ -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();
}
+
///<inheritdoc/>
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
{
/// <summary>
- /// The service domain controller that manages all
- /// servers for an application based on a
- /// <see cref="ServiceDomain"/>
+ /// An HTTP servicing stack that manages a collection of HTTP servers
+ /// their service domain
/// </summary>
public sealed class HttpServiceStack : VnDisposeable
{
@@ -48,7 +53,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack
/// <summary>
/// The service domain's plugin controller
/// </summary>
- public IPluginController PluginController => _serviceDomain;
+ public IPluginManager PluginManager => _serviceDomain.PluginManager;
/// <summary>
/// Initializes a new <see cref="HttpServiceStack"/> 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<Task> 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
/// <returns>The task that completes when</returns>
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();
}
///<inheritdoc/>
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,16 +34,21 @@ namespace VNLib.Plugins.Essentials.ServiceStack
/// Represents a live plugin controller that manages all
/// plugins loaded in a <see cref="ServiceDomain"/>
/// </summary>
- public interface IPluginController
+ public interface IPluginManager
{
/// <summary>
+ /// The the plugins managed by this <see cref="IPluginManager"/>
+ /// </summary>
+ public IEnumerable<IManagedPlugin> Plugins { get; }
+
+ /// <summary>
/// Loads all plugins specified by the host config to the service manager,
/// or attempts to load plugins by the default
/// </summary>
/// <param name="config">The configuration instance to pass to plugins</param>
/// <param name="appLog">A log provider to write message and errors to</param>
/// <returns>A task that resolves when all plugins are loaded</returns>
- Task LoadPlugins(JsonDocument config, ILogProvider appLog);
+ Task LoadPluginsAsync(PluginLoadConfiguration config, ILogProvider appLog);
/// <summary>
/// Sends a message to a plugin identified by it's name.
@@ -61,11 +68,8 @@ namespace VNLib.Plugins.Essentials.ServiceStack
void ForceReloadAllPlugins();
/// <summary>
- /// Unloads all service groups, removes them, and unloads all
- /// loaded plugins
+ /// Unloads all loaded plugins and calls thier event handlers
/// </summary>
- /// <exception cref="AggregateException"></exception>
- /// <exception cref="ObjectDisposedException"></exception>
- 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
+{
+
+ /// <summary>
+ /// Represents a plugin managed by a <see cref="IPluginManager"/> that includes dynamically loaded plugins
+ /// </summary>
+ public interface IManagedPlugin
+ {
+ /// <summary>
+ /// Exposes the internal <see cref="PluginController"/> for the loaded plugin
+ /// </summary>
+ PluginController Controller { get; }
+
+ /// <summary>
+ /// The file path to the loaded plugin
+ /// </summary>
+ string PluginPath { get; }
+
+ /// <summary>
+ /// The exposed services the inernal plugin provides
+ /// </summary>
+ /// <remarks>
+ /// 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.
+ /// </remarks>
+ 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
{
/// <summary>
- /// Represents a host that exposes a processor for host events
+ /// Represents an HTTP service host which provides information required
+ /// for HttpServer routing and the <see cref="IWebRoot"/> for proccessing
+ /// incomming connections
/// </summary>
public interface IServiceHost
{
/// <summary>
- /// The <see cref="EventProcessor"/> to process
- /// incoming HTTP connections
+ /// The <see cref="IWebRoot"/> that handles HTTP connection
+ /// processing.
/// </summary>
- EventProcessor Processor { get; }
+ IWebRoot Processor { get; }
+
/// <summary>
/// The host's transport infomration
/// </summary>
IHostTransportInfo TransportInfo { get; }
+
+ /// <summary>
+ /// Called when a plugin is loaded and is endpoints are extracted
+ /// to be placed into service.
+ /// </summary>
+ /// <param name="plugin">The loaded plugin ready to be attached</param>
+ /// <param name="endpoints">The dynamic endpoints of a loading plugin</param>
+ void OnRuntimeServiceAttach(IManagedPlugin plugin, IEndpoint[] endpoints);
+
+ /// <summary>
+ /// Called when a <see cref="ServiceDomain"/>'s <see cref="IPluginManager"/>
+ /// unloads a given plugin, and its originally discovered endpoints
+ /// </summary>
+ /// <param name="plugin">The unloading plugin to detach</param>
+ /// <param name="endpoints">The endpoints of the unloading plugin to remove from service</param>
+ 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
+{
+ /// <summary>
+ /// A <see cref="IServiceProvider"/> that may be unloaded when the
+ /// assembly that is sharing the types are being disposed.
+ /// </summary>
+ public interface IUnloadableServiceProvider : IServiceProvider
+ {
+ /// <summary>
+ /// A token that is set cancelled state when the service provider
+ /// is unloaded.
+ /// </summary>
+ 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;
+ }
+
+ ///<inheritdoc/>
+ public string PluginPath { get; }
+
+ ///<inheritdoc/>
+ public IUnloadableServiceProvider Services
+ {
+ get
+ {
+ Check();
+ return _services!;
+ }
+ }
+
+ ///<inheritdoc/>
+ 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<ServiceConfiguratorAttribute>() != null && !m.IsAbstract)
+ .Select(m => m.CreateDelegate<ServiceConfigurator>(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();
+ }
+
+ ///<inheritdoc/>
+ CancellationToken IUnloadableServiceProvider.UnloadToken => _cts.Token;
+
+ /// <summary>
+ /// Signals to listensers that the service container will be unloading
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// Plugin loading configuration variables
+ /// </summary>
+ public readonly record struct PluginLoadConfiguration
+ {
+ /// <summary>
+ /// The directory containing the dynamic plugin assemblies to load
+ /// </summary>
+ public readonly string PluginDir { get; init; }
+
+ /// <summary>
+ /// A value that indicates if the internal <see cref="PluginController"/>
+ /// allows for hot-reload/unloadable plugin assemblies.
+ /// </summary>
+ public readonly bool HotReload { get; init; }
+
+ /// <summary>
+ /// The optional host configuration file to merge with plugin config
+ /// to pass to the loading plugin.
+ /// </summary>
+ public readonly JsonDocument? HostConfig { get; init; }
+
+ /// <summary>
+ /// Passed to the underlying <see cref="RuntimePluginLoader"/>
+ /// holding plugins
+ /// </summary>
+ 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
+{
+
+ /// <summary>
+ /// A sealed type that manages the plugin interaction layer. Manages the lifetime of plugin
+ /// instances, exposes controls, and relays stateful plugin events.
+ /// </summary>
+ internal sealed class PluginManager : VnDisposeable, IPluginManager, IPluginEventListener
+ {
+ private const string PLUGIN_FILE_EXTENSION = ".dll";
+
+ private readonly List<ManagedPlugin> _plugins;
+ private readonly IReadOnlyCollection<ServiceGroup> _dependents;
+
+
+ private IEnumerable<LivePlugin> _livePlugins => _plugins.SelectMany(static p => p.Controller.Plugins);
+
+ /// <summary>
+ /// The collection of internal controllers
+ /// </summary>
+ public IEnumerable<IManagedPlugin> Plugins => _plugins;
+
+ public PluginManager(IReadOnlyCollection<ServiceGroup> dependents)
+ {
+ _plugins = new();
+ _dependents = dependents;
+ }
+
+ /// <inheritdoc/>
+ /// <exception cref="ObjectDisposedException"></exception>
+ 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<DirectoryInfo> dirs = dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly);
+
+ //Select only dirs with a dll that is named after the directory name
+ IEnumerable<string> pluginPaths = GetPluginPaths(dirs);
+
+ IEnumerable<string> 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<string> GetPluginPaths(IEnumerable<DirectoryInfo> 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();
+ }
+ }
+
+ /// <inheritdoc/>
+ 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;
+ }
+
+ /// <inheritdoc/>
+ public void ForceReloadAllPlugins()
+ {
+ //Reload all plugin managers
+ _plugins.TryForeach(static p => p.ReloadPlugins());
+ }
+
+ /// <inheritdoc/>
+ 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,39 +24,23 @@
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
{
+
/// <summary>
/// Represents a domain of services and thier dynamically loaded plugins
/// that will be hosted by an application service stack
/// </summary>
- 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<ServiceGroup> _serviceGroups;
- private readonly LinkedList<RuntimePluginLoader> _pluginLoaders;
-
- /// <summary>
- /// Enumerates all loaded plugin instances
- /// </summary>
- public IEnumerable<IPlugin> Plugins => _pluginLoaders.SelectMany(static s =>
- s.LivePlugins.Where(static p => p.Plugin != null)
- .Select(static s => s.Plugin!)
- );
+ private readonly PluginManager _plugins;
/// <summary>
/// Gets all service groups loaded in the service manager
@@ -64,12 +48,19 @@ namespace VNLib.Plugins.Essentials.ServiceStack
public IReadOnlyCollection<ServiceGroup> ServiceGroups => _serviceGroups;
/// <summary>
+ /// Gets the internal <see cref="IPluginManager"/> that manages plugins for the entire
+ /// <see cref="ServiceDomain"/>
+ /// </summary>
+ public IPluginManager PluginManager => _plugins;
+
+ /// <summary>
/// Initializes a new empty <see cref="ServiceDomain"/>
/// </summary>
public ServiceDomain()
{
_serviceGroups = new();
- _pluginLoaders = new();
+ //Init plugin manager and pass ref to service group collection
+ _plugins = new PluginManager(_serviceGroups);
}
/// <summary>
@@ -78,8 +69,11 @@ namespace VNLib.Plugins.Essentials.ServiceStack
/// </summary>
/// <param name="hostBuilder">The callback method to build virtual hosts</param>
/// <returns>A value that indicates if any virtual hosts were successfully loaded</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
public bool BuildDomain(Action<ICollection<IServiceHost>> hostBuilder)
{
+ Check();
+
//LL to store created hosts
LinkedList<IServiceHost> hosts = new();
@@ -94,8 +88,11 @@ namespace VNLib.Plugins.Essentials.ServiceStack
/// </summary>
/// <param name="hosts">The enumeration of virtual hosts</param>
/// <returns>A value that indicates if any virtual hosts were successfully loaded</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
public bool FromExisting(IEnumerable<IServiceHost> 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<IServiceHost> 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
}
}
- ///<inheritdoc/>
- 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<DirectoryInfo> dirs = dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly);
-
- //Select only dirs with a dll that is named after the directory name
- IEnumerable<string> 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<string> pluginFileNames = pluginPaths.Select(static s => $"{Path.GetFileName(s)}\n");
-
- appLog.Debug("Found plugin files: \n{files}", string.Concat(pluginFileNames));
-
- LinkedList<Task> 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);
- }
-
- ///<inheritdoc/>
- 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;
- }
-
- ///<inheritdoc/>
- public void ForceReloadAllPlugins()
- {
- Check();
- _pluginLoaders.TryForeach(static pl => pl.ReloadPlugin());
- }
-
- ///<inheritdoc/>
- public void UnloadAll()
+ /// <summary>
+ /// Tears down the service domain by unloading all plugins (calling their event handlers)
+ /// and destroying all <see cref="ServiceGroup"/>s. This instance may be rebuilt if this
+ /// method returns successfully.
+ /// </summary>
+ 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<ISessionProvider>())
- .SingleOrDefault();
-
- //If session provider has been supplied, load it
- if (sessionLoader != null)
- {
- //Get the session provider from the plugin loader
- ISessionProvider sp = sessionLoader.GetExposedTypeFromPlugin<ISessionProvider>()!;
-
- //Init inital provider
- onSessionProviderReloaded(null!, sp);
-
- //Register reload event
- sessionLoader.RegisterListenerForSingle<ISessionProvider>(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<IPageRouter>())
- .SingleOrDefault();
-
- //If router has been supplied, load it
- if (routerLoader != null)
- {
- //Get initial value
- IPageRouter sp = routerLoader.GetExposedTypeFromPlugin<IPageRouter>()!;
-
- //Init inital provider
- onRouterReloaded(null!, sp);
-
- //Register reload event
- routerLoader.RegisterListenerForSingle<IPageRouter>(onRouterReloaded);
- }
- }
- catch (InvalidOperationException)
- {
- throw new TypeLoadException("More than one page router plugin was defined in the plugin directory, cannot continue");
- }
}
+
///<inheritdoc/>
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<IServiceHost> _vHosts;
- private readonly ConditionalWeakTable<RuntimePluginLoader, IEndpoint[]> _endpointsForPlugins;
+ private readonly ConditionalWeakTable<IManagedPlugin, IEndpoint[]> _endpointsForPlugins;
/// <summary>
/// The <see cref="IPEndPoint"/> transport endpoint for all loaded service hosts
@@ -68,61 +66,43 @@ namespace VNLib.Plugins.Essentials.ServiceStack
}
/// <summary>
- /// Sets the specified page rotuer for all virtual hosts
+ /// Manually detatches runtime services and their loaded endpoints from all
+ /// endpoints.
/// </summary>
- /// <param name="router">The page router to user</param>
- internal void UpdatePageRouter(IPageRouter router) => _vHosts.TryForeach(v => v.Processor.SetPageRouter(router));
- /// <summary>
- /// Sets the specified session provider for all virtual hosts
- /// </summary>
- /// <param name="current">The session provider to use</param>
- 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)));
- /// <summary>
- /// 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
- /// </summary>
- /// <param name="loader">The plugin loader to get add/update endpoints from</param>
- 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));
}
- /// <summary>
- /// Unloads all previously stored endpoints, router, session provider, and
- /// clears all internal data structures
- /// </summary>
- 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 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
- <ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>VNLib.Plugins.Essentials.ServiceStack</RootNamespace>
<AssemblyName>VNLib.Plugins.Essentials.ServiceStack</AssemblyName>
<Nullable>enable</Nullable>
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
/// </summary>
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;
/// <summary>
- /// The maximum time in seconds for a login message to be considered valid
- /// </summary>
- public const double MAX_TIME_DIFF_SECS = 10.00;
- /// <summary>
/// The size in bytes of the random passwords generated when invoking the <see cref="SetRandomPasswordAsync(PasswordHashing, IUserManager, IUser, int)"/>
/// </summary>
public const int RANDOM_PASS_SIZE = 128;
- /// <summary>
- /// The name of the header that will identify a client's identiy
- /// </summary>
- public const string LOGIN_TOKEN_HEADER = "X-Web-Token";
+
/// <summary>
/// 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
/// </summary>
public const string LOCAL_ACCOUNT_ORIGIN = "local";
- /// <summary>
- /// The size (in bytes) of the challenge secret
- /// </summary>
- public const int CHALLENGE_SIZE = 64;
- /// <summary>
- /// The size (in bytes) of the sesssion long user-password challenge
- /// </summary>
- 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;
- /// <summary>
- /// The name of the login cookie set when a user logs in
- /// </summary>
- public const string LOGIN_COOKIE_NAME = "VNLogin";
- /// <summary>
- /// The name of the login client identifier cookie (cookie that is set fir client to use to determine if the user is logged in)
- /// </summary>
- 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);
-
- /// <summary>
- /// The client data encryption padding.
- /// </summary>
- public static readonly RSAEncryptionPadding ClientEncryptonPadding = RSAEncryptionPadding.OaepSHA256;
-
- /// <summary>
- /// The size (in bytes) of the web-token hash size
- /// </summary>
- private static readonly int TokenHashSize = (SHA384.Create().HashSize / 8);
/// <summary>
/// Speical character regual expresion for basic checks
@@ -154,7 +103,7 @@ namespace VNLib.Plugins.Essentials.Accounts
/// <exception cref="VnArgon2Exception"></exception>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="ArgumentNullException"></exception>
- public static async Task<ERRNO> SetRandomPasswordAsync(this PasswordHashing passHashing, IUserManager manager, IUser user, int size = RANDOM_PASS_SIZE)
+ public static async Task<ERRNO> 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
-
/// <summary>
- /// 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
/// </summary>
- /// <param name="ev">The connection and session to log-in</param>
- /// <param name="loginMessage">The message of the client to set the log-in status of</param>
- /// <param name="user">The user to log-in</param>
- /// <returns>The encrypted base64 token secret data to send to the client</returns>
- /// <exception cref="OutOfMemoryException"></exception>
- /// <exception cref="CryptographicException"></exception>
- public static string GenerateAuthorization(this HttpEntity ev, LoginMessage loginMessage, IUser user)
+ /// <param name="hashing"></param>
+ /// <param name="size">The size (in bytes) of the new random password</param>
+ /// <returns>A <see cref="PrivateString"/> that contains the new password hash</returns>
+ 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<byte>.Shared.Rent(size);
+ try
+ {
+ Span<byte> span = randBuffer.AsSpan(0, size);
- /// <summary>
- /// Runs necessary operations to grant authorization to the specified user of a given session and user with provided variables
- /// </summary>
- /// <param name="ev">The connection and session to log-in</param>
- /// <param name="base64PubKey">The clients base64 public key</param>
- /// <param name="clientId">The browser/client id</param>
- /// <param name="user">The user to log-in</param>
- /// <returns>The encrypted base64 token secret data to send to the client</returns>
- /// <exception cref="OutOfMemoryException"></exception>
- /// <exception cref="CryptographicException"></exception>
- /// <exception cref="InvalidOperationException"></exception>
- 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<byte>.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
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <param name="manager"></param>
+ /// <param name="userId">The id of the user to check the password against</param>
+ /// <param name="rawPassword">The raw password of the user to compare hashes against</param>
+ /// <param name="hashing">The password hashing tools</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>A task that completes with the value of the password hashing match.</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static async Task<bool> VerifyPasswordAsync(this IUserManager manager, string userId, PrivateString rawPassword, IPasswordHashingProvider hashing, CancellationToken cancellation)
{
- public readonly Span<byte> Buffer { private get; init; }
- public readonly Span<byte> SignatureBuffer => Buffer[..64];
-
-
-
- public int ClientPbkWritten;
- public readonly Span<byte> ClientPublicKeyBuffer => Buffer.Slice(64, 1024);
- public readonly ReadOnlySpan<byte> 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<byte> ClientEncOutputBuffer => Buffer[(64 + 1024)..];
- public readonly ReadOnlySpan<byte> EncryptedOutput => ClientEncOutputBuffer[..ClientEncBytesWritten];
+ return user != null && hashing.Verify(user.PassHash.ToReadOnlySpan(), rawPassword.ToReadOnlySpan());
}
-
+
/// <summary>
- /// 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
+ /// <see cref="PasswordHashing"/> instance
/// </summary>
- /// <param name="base64clientPublicKey">The user's public key credential</param>
- /// <param name="base64Digest">The base64 encoded digest of the secret that was encrypted</param>
- /// <param name="base64ClientData">The client's user-agent header value</param>
- /// <returns>A string representing a unique signed token for a given login context</returns>
- /// <exception cref="OutOfMemoryException"></exception>
- /// <exception cref="CryptographicException"></exception>
- private static void TryGenerateToken(string base64clientPublicKey, out string base64Digest, out string base64ClientData)
+ /// <param name="user"></param>
+ /// <param name="rawPassword"></param>
+ /// <param name="hashing">The <see cref="IPasswordHashingProvider"/> provider instance</param>
+ /// <returns>True if the password </returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool VerifyPassword(this IUser user, PrivateString rawPassword, IPasswordHashingProvider hashing)
{
- //Temporary work buffer
- using IMemoryHandle<byte> buffer = MemoryUtil.SafeAlloc<byte>(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);
}
/// <summary>
- /// Determines if the client sent a token header, and it maches against the current session
+ /// Verifies a password against its previously encoded hash.
/// </summary>
- /// <returns>true if the client set the token header, the session is loaded, and the token matches the session, false otherwise</returns>
- public static bool TokenMatches(this HttpEntity ev)
+ /// <param name="provider"></param>
+ /// <param name="passHash">Previously hashed password</param>
+ /// <param name="password">Raw password to compare against</param>
+ /// <returns>True if bytes derrived from password match the hash, false otherwise</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ [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<byte> buffer = MemoryUtil.UnsafeAlloc<byte>(TokenHashSize * 2, true);
- //Slice up buffers
- Span<byte> headerBuffer = buffer.Span[..TokenHashSize];
- Span<byte> 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<char>)passHash, (ReadOnlySpan<char>)password);
}
/// <summary>
- /// 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.
/// </summary>
- /// <returns>The base64 of the newly encrypted secret</returns>
- public static string? RegenerateClientToken(this HttpEntity ev)
+ /// <param name="provider"></param>
+ /// <param name="password">Password to be hashed</param>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <returns>A <see cref="PrivateString"/> of the hashed and encoded password</returns>
+ [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<char>)password);
}
- /// <summary>
- /// Tries to encrypt the specified data using the stored public key and store the encrypted data into
- /// the output buffer.
- /// </summary>
- /// <param name="session"></param>
- /// <param name="data">Data to encrypt</param>
- /// <param name="outputBuffer">The buffer to store encrypted data in</param>
- /// <returns>
- /// The number of encrypted bytes written to the output buffer,
- /// or false (0) if the operation failed, or if no credential is
- /// stored.
- /// </returns>
- /// <exception cref="CryptographicException"></exception>
- public static ERRNO TryEncryptClientData(this in SessionInfo session, ReadOnlySpan<byte> data, in Span<byte> 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");
}
+
/// <summary>
- /// 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
/// </summary>
- /// <param name="base64PubKey">A base64 encoded public key used to encrypt client data</param>
- /// <param name="data">Data to encrypt</param>
- /// <param name="outputBuffer">The buffer to store encrypted data in</param>
- /// <returns>
- /// The number of encrypted bytes written to the output buffer,
- /// or false (0) if the operation failed, or if no credential is
- /// specified.
- /// </returns>
- /// <exception cref="CryptographicException"></exception>
- public static ERRNO TryEncryptClientData(ReadOnlySpan<char> base64PubKey, ReadOnlySpan<byte> data, in Span<byte> outputBuffer)
+ /// <param name="entity"></param>
+ /// <param name="mode">The authoziation level</param>
+ /// <returns>True if the connection has the desired authorization status</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ [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<byte> pubKeyBuffer = MemoryUtil.UnsafeAlloc<byte>(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);
}
+
/// <summary>
- /// 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
/// </summary>
- /// <param name="rawPubKey">The raw SKI public key</param>
- /// <param name="data">Data to encrypt</param>
- /// <param name="outputBuffer">The buffer to store encrypted data in</param>
- /// <returns>
- /// The number of encrypted bytes written to the output buffer,
- /// or false (0) if the operation failed, or if no credential is
- /// specified.
- /// </returns>
+ /// <param name="entity">The connection and session to log-in</param>
+ /// <param name="secInfo">The clients login security information</param>
+ /// <param name="user">The user to log-in</param>
+ /// <returns>The encrypted base64 token secret data to send to the client</returns>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
/// <exception cref="CryptographicException"></exception>
- public static ERRNO TryEncryptClientData(ReadOnlySpan<byte> rawPubKey, ReadOnlySpan<byte> data, in Span<byte> outputBuffer)
+ /// <exception cref="InvalidOperationException"></exception>
+ 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;
- }
- /// <summary>
- /// Stores the clients public key specified during login
- /// </summary>
- /// <param name="session"></param>
- /// <param name="base64PubKey"></param>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void SetBrowserPubKey(in SessionInfo session, string base64PubKey) => session[CLIENT_PUB_KEY_ENTRY] = base64PubKey;
+ IAccountSecurityProvider provider = entity.GetSecProviderOrThrow();
- /// <summary>
- /// Gets the clients stored public key that was specified during login
- /// </summary>
- /// <returns>The base64 encoded public key string specified at login</returns>
- [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;
+ }
/// <summary>
- /// Stores the login key as a cookie in the current session as long as the session exists
- /// </summary>/
- /// <param name="ev">The event to log-in</param>
- /// <param name="localAccount">Does the session belong to a local user account</param>
+ /// Generates a client authorization from the supplied security info
+ /// using the default <see cref="IAccountSecurityProvider"/> and
+ /// stored the required variables in the <paramref name="response"/>
+ /// response <see cref="WebMessage"/>
+ /// </summary>
+ /// <param name="entity"></param>
+ /// <param name="secInfo">The client's <see cref="IClientSecInfo"/> used to authorize the client</param>
+ /// <param name="user">The user requesting the authenticated use</param>
+ /// <param name="response">The response to store variables in</param>
+ /// <exception cref="NotSupportedException"></exception>
[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;
}
/// <summary>
- /// 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
/// </summary>
+ /// <param name="entity"></param>
+ /// <returns>The new <see cref="IClientAuthorization"/> for the regenerated credentials</returns>
+ /// <exception cref="NotSupportedException"></exception>
[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);
}
/// <summary>
- /// 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
/// </summary>
- /// <returns>True if the session is active, the cookie was properly received, and the cookie value matches the session. False otherwise</returns>
- 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<byte> buffer = MemoryUtil.UnsafeAlloc<byte>(2 * LOGIN_COOKIE_SIZE, true);
- //Slice up buffers
- Span<byte> cookieBuffer = buffer.Span[..LOGIN_COOKIE_SIZE];
- Span<byte> 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;
+ /// <param name="entity"></param>
+ /// <param name="response">The response message to return to the client</param>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <returns>The new <see cref="IClientAuthorization"/> for the regenerated credentials</returns>
+ 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();
}
-
+
/// <summary>
- /// 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
/// </summary>
- /// <param name="ev"></param>
- public static void ReconcileCookies(this HttpEntity ev)
+ /// <param name="entity"></param>
+ /// <param name="data">The data to encrypt for the current client</param>
+ /// <param name="output">The buffer to write encypted data to</param>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <returns>The number of bytes encrypted and written to the output buffer</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ERRNO TryEncryptClientData(this HttpEntity entity, ReadOnlySpan<byte> data, Span<byte> 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);
}
/// <summary>
- /// 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
/// </summary>
- /// <param name="session"></param>
- /// <returns>The last time the token was updated/generated, or <see cref="DateTimeOffset.MinValue"/> if not set</returns>
- public static DateTimeOffset LastTokenUpgrade(this in SessionInfo session)
+ /// <param name="entity"></param>
+ /// <param name="secInfo">Used for unauthorized connections to encrypt client data based on client security info</param>
+ /// <param name="data">The data to encrypt for the current client</param>
+ /// <param name="output">The buffer to write encypted data to</param>
+ /// <returns>The number of bytes encrypted and written to the output buffer</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static ERRNO TryEncryptClientData(this HttpEntity entity, IClientSecInfo secInfo, ReadOnlySpan<byte> data, Span<byte> 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));
- /// <summary>
- /// Updates the last time the session token was set
- /// </summary>
- /// <param name="session"></param>
- /// <param name="updated">The UTC time the last token was set</param>
- 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);
+ }
/// <summary>
- /// Stores the browser's id during a login process
+ /// Invalidates the login status of the current connection and session (if session is loaded)
/// </summary>
- /// <param name="session"></param>
- /// <param name="browserId">Browser id value to store</param>
+ /// <exception cref="NotSupportedException"></exception>
[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();
+ }
/// <summary>
/// Gets the current browser's id if it was specified during login process
@@ -728,7 +457,8 @@ namespace VNLib.Plugins.Essentials.Accounts
/// <param name="session"></param>
/// <param name="value">True for a local account, false otherwise</param>
[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;
+
/// <summary>
/// Gets a value indicating if the session belongs to a local user account
/// </summary>
@@ -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
- */
-
- /// <summary>
- /// Generates a new password challenge for the current session and specified password
- /// </summary>
- /// <param name="session"></param>
- /// <param name="password">The user's password to compute the hash of</param>
- /// <returns>The raw derrivation key to send to the client</returns>
- public static byte[] GenPasswordChallenge(this in SessionInfo session, PrivateString password)
- {
- ReadOnlySpan<char> rawPass = password;
- //Calculate the password buffer size required
- int passByteCount = Encoding.UTF8.GetByteCount(rawPass);
- //Allocate the buffer
- using UnsafeMemoryHandle<byte> bufferHandle = MemoryUtil.UnsafeAlloc<byte>(passByteCount + 64, true);
- //Slice buffers
- Span<byte> utf8PassBytes = bufferHandle.Span[..passByteCount];
- Span<byte> 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);
- }
- }
- /// <summary>
- /// Verifies the stored unique digest of the user's password against
- /// the client derrived password
- /// </summary>
- /// <param name="session"></param>
- /// <param name="base64PasswordDigest">The base64 client derrived digest of the user's password to verify</param>
- /// <returns>True if formatting was correct and the derrived passwords match, false otherwise</returns>
- /// <exception cref="FormatException"></exception>
- public static bool VerifyChallenge(this in SessionInfo session, ReadOnlySpan<char> base64PasswordDigest)
- {
- string base32Digest = session[CHALLENGE_HMAC_ENTRY];
- if (string.IsNullOrWhiteSpace(base32Digest))
- {
- return false;
- }
- int bufSize = base32Digest.Length + base64PasswordDigest.Length;
- //Alloc buffer
- using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAlloc<byte>(bufSize);
- //Split buffers
- Span<byte> localBuf = buffer.Span[..base32Digest.Length];
- Span<byte> 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
/// <summary>
/// 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
+{
+ /// <summary>
+ /// Specifies how critical the security check is for a user to access
+ /// a given resource
+ /// </summary>
+ public enum AuthorzationCheckLevel
+ {
+ /// <summary>
+ /// No authorization check is required.
+ /// </summary>
+ None,
+ /// <summary>
+ /// Is there any information that the client may have authorization. NOTE: Not a security check!
+ /// </summary>
+ Any,
+ /// <summary>
+ /// 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.
+ /// </summary>
+ Medium,
+ /// <summary>
+ /// The a full authorization check is required as the user may access
+ /// secure resouces.
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// A structure that contains the client/server information
+ /// for client/server authorization
+ /// </summary>
+ /// <param name="ClientToken">
+ /// The public portion of the token to send to the client
+ /// </param>
+ /// <param name="ServerToken">
+ /// The secret portion of the token that is to be
+ /// stored on the server (usually in the client's session)
+ /// </param>
+ 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
+{
+ /// <summary>
+ /// Provides account security to client connections. Providing authoirzation,
+ /// verification, and client data encryption.
+ /// </summary>
+ public interface IAccountSecurityProvider
+ {
+ /// <summary>
+ /// Generates a new authorization for the connection with its client security information
+ /// </summary>
+ /// <param name="entity">The connection to authorize</param>
+ /// <param name="clientInfo">The client security information required for authorization</param>
+ /// <param name="user">The user object to authorize the connection for</param>
+ /// <returns>The new authorization information for the connection</returns>
+ IClientAuthorization AuthorizeClient(HttpEntity entity, IClientSecInfo clientInfo, IUser user);
+
+ /// <summary>
+ /// Regenerates the client's authorization status for a currently logged-in user
+ /// </summary>
+ /// <param name="entity">The connection to re-authorize</param>
+ /// <returns>The new <see cref="IClientAuthorization"/> containing the new authorization information</returns>
+ IClientAuthorization ReAuthorizeClient(HttpEntity entity);
+
+ /// <summary>
+ /// Determines if the connection is considered authorized for the desired
+ /// security level
+ /// </summary>
+ /// <param name="entity">The connection to determine the status of</param>
+ /// <param name="level">The authorziation level to check for</param>
+ /// <returns>True if the given connection meets the desired authorzation status</returns>
+ bool IsClientAuthorized(HttpEntity entity, AuthorzationCheckLevel level);
+
+ /// <summary>
+ /// Encryptes data using the stored client's authorization information.
+ /// </summary>
+ /// <param name="entity">The connection to encrypt data for</param>
+ /// <param name="data">The data to encrypt</param>
+ /// <param name="outputBuffer">The buffer to write the encrypted data to</param>
+ /// <returns>The number of bytes written to the output buffer, or o/false if the data could not be encrypted</returns>
+ ERRNO TryEncryptClientData(HttpEntity entity, ReadOnlySpan<byte> data, Span<byte> outputBuffer);
+
+ /// <summary>
+ /// Attempts a one-time encryption of client data for a non-authorized user
+ /// based on the client's <see cref="IClientSecInfo"/> data.
+ /// </summary>
+ /// <param name="clientSecInfo">The client's <see cref="IClientSecInfo"/> credentials used to encrypt the message</param>
+ /// <param name="data">The data to encrypt</param>
+ /// <param name="outputBuffer">The output buffer to write encrypted data to</param>
+ /// <returns>The number of bytes written to the output buffer, 0/false if the operation failed</returns>
+ ERRNO TryEncryptClientData(IClientSecInfo clientSecInfo, ReadOnlySpan<byte> data, Span<byte> outputBuffer);
+
+ /// <summary>
+ /// Invalidates a logged in connection
+ /// </summary>
+ /// <param name="entity">The connection to invalidate the login status of</param>
+ 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
+{
+ /// <summary>
+ /// Contains the client's minimum authorization variables
+ /// </summary>
+ public interface IClientAuthorization
+ {
+ /// <summary>
+ /// A security token that may be set as a cookie or used
+ /// </summary>
+ string? LoginSecurityString { get; }
+
+ /// <summary>
+ /// The clients security token information
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// Exposed the required security information for a <see cref="IAccountSecurityProvider"/>
+ /// to authorized a connection.
+ /// </summary>
+ public interface IClientSecInfo
+ {
+ /// <summary>
+ /// The clients public-key
+ /// </summary>
+ string PublicKey { get; }
+
+ /// <summary>
+ /// The unique id the client provided to this server
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// Represents a common abstraction for password hashing providers/libraries
+ /// </summary>
+ public interface IPasswordHashingProvider
+ {
+ /// <summary>
+ /// Verifies a password against its previously encoded hash.
+ /// </summary>
+ /// <param name="passHash">Previously hashed password</param>
+ /// <param name="password">Raw password to compare against</param>
+ /// <returns>true if bytes derrived from password match the hash, false otherwise</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ bool Verify(ReadOnlySpan<char> passHash, ReadOnlySpan<char> password);
+
+ /// <summary>
+ /// Verifies a password against its previously encoded hash.
+ /// </summary>
+ /// <param name="passHash">Previously hashed password in binary</param>
+ /// <param name="password">Raw password to compare against the hash</param>
+ /// <returns>true if bytes derrived from password match the hash, false otherwise</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ bool Verify(ReadOnlySpan<byte> passHash, ReadOnlySpan<byte> password);
+
+ /// <summary>
+ /// Hashes the specified character encoded password to it's secured hashed form.
+ /// </summary>
+ /// <param name="password">The character encoded password to encrypt</param>
+ /// <returns>A <see cref="PrivateString"/> containing the new password hash.</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ PrivateString Hash(ReadOnlySpan<char> password);
+
+ /// <summary>
+ /// Hashes the specified binary encoded password to it's secured hashed form.
+ /// </summary>
+ /// <param name="password">The binary encoded password to encrypt</param>
+ /// <returns>A <see cref="PrivateString"/> containing the new password hash.</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ PrivateString Hash(ReadOnlySpan<byte> password);
+
+ /// <summary>
+ /// Exposes a lower level for producing a password hash and writing it to the output buffer
+ /// </summary>
+ /// <param name="password">The raw password to encrypt</param>
+ /// <param name="hashOutput">The output buffer to write encoded data into</param>
+ /// <returns>The number of bytes written to the hash buffer, or 0/false if the hashing operation failed</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ ERRNO Hash(ReadOnlySpan<byte> password, Span<byte> 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 <see cref="PrivateStringManager"/>
/// and should be disposed properly
/// </remarks>
- public class LoginMessage : PrivateStringManager
+ public class LoginMessage : PrivateStringManager, IClientSecInfo
{
/// <summary>
/// A property
@@ -80,7 +80,8 @@ namespace VNLib.Plugins.Essentials.Accounts
/// The clients browser id if shared
/// </summary>
[JsonPropertyName("clientid")]
- public string ClientID { get; set; }
+ public string ClientId { get; set; }
+
/// <summary>
/// Initailzies a new <see cref="LoginMessage"/> and its parent <see cref="PrivateStringManager"/>
/// base
@@ -98,5 +99,10 @@ namespace VNLib.Plugins.Essentials.Accounts
/// or access to <see cref="Password"/> will throw
/// </remarks>
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
+{
+ /// <summary>
+ /// A password pased client/server challenge
+ /// </summary>
+ /// <param name="ClientData">The client portion of the password based challenge</param>
+ /// <param name="ServerData">The server potion of the password based challenge</param>
+ 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
{
+
/// <summary>
/// Provides a structured password hashing system implementing the <seealso cref="VnArgon2"/> library
/// with fixed time comparison
/// </summary>
- 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;
}
-
- /// <summary>
- /// Verifies a password against its previously encoded hash.
- /// </summary>
- /// <param name="passHash">Previously hashed password</param>
- /// <param name="password">Raw password to compare against</param>
- /// <returns>true if bytes derrived from password match the hash, false otherwise</returns>
- /// <exception cref="FormatException"></exception>
- /// <exception cref="VnArgon2Exception"></exception>
- /// <exception cref="VnArgon2PasswordFormatException"></exception>
- public bool Verify(PrivateString passHash, PrivateString password)
- {
- //Casting PrivateStrings to spans will reference the base string directly
- return Verify((ReadOnlySpan<char>)passHash, (ReadOnlySpan<char>)password);
- }
- /// <summary>
- /// Verifies a password against its previously encoded hash.
- /// </summary>
- /// <param name="passHash">Previously hashed password</param>
- /// <param name="password">Raw password to compare against</param>
- /// <returns>true if bytes derrived from password match the hash, false otherwise</returns>
- /// <exception cref="FormatException"></exception>
- /// <exception cref="VnArgon2Exception"></exception>
- /// <exception cref="VnArgon2PasswordFormatException"></exception>
+
+ ///<inheritdoc/>
+ ///<exception cref="VnArgon2Exception"></exception>
+ ///<exception cref="VnArgon2PasswordFormatException"></exception>
public bool Verify(ReadOnlySpan<char> passHash, ReadOnlySpan<char> password)
{
if(passHash.IsEmpty || password.IsEmpty)
{
return false;
}
- //alloc secret buffer
- using UnsafeMemoryHandle<byte> secretBuffer = MemoryUtil.UnsafeAlloc<byte>(_secret.BufferSize, true);
+
+ if(_secret.BufferSize < STACK_MAX_BUFF_SIZE)
+ {
+ //Alloc stack buffer
+ Span<byte> secretBuffer = stackalloc byte[STACK_MAX_BUFF_SIZE];
+
+ return VerifyInternal(passHash, password, secretBuffer);
+ }
+ else
+ {
+ //Alloc heap buffer
+ using UnsafeMemoryHandle<byte> secretBuffer = MemoryUtil.UnsafeAlloc<byte>(_secret.BufferSize, true);
+
+ return VerifyInternal(passHash, password, secretBuffer);
+ }
+ }
+
+ private bool VerifyInternal(ReadOnlySpan<char> passHash, ReadOnlySpan<char> password, Span<byte> 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);
}
}
-
+
/// <summary>
/// Verifies a password against its hash. Partially exposes the Argon2 api.
/// </summary>
@@ -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);
}
-
- /// <summary>
- /// Hashes a specified password, with the initialized pepper, and salted with CNG random bytes.
- /// </summary>
- /// <param name="password">Password to be hashed</param>
- /// <exception cref="VnArgon2Exception"></exception>
- /// <returns>A <see cref="PrivateString"/> of the hashed and encoded password</returns>
- public PrivateString Hash(PrivateString password) => Hash((ReadOnlySpan<char>)password);
- /// <summary>
- /// Hashes a specified password, with the initialized pepper, and salted with CNG random bytes.
- /// </summary>
- /// <param name="password">Password to be hashed</param>
+ /// <inheritdoc/>
/// <exception cref="VnArgon2Exception"></exception>
/// <returns>A <see cref="PrivateString"/> of the hashed and encoded password</returns>
public PrivateString Hash(ReadOnlySpan<char> password)
@@ -175,11 +164,8 @@ namespace VNLib.Plugins.Essentials.Accounts
MemoryUtil.InitializeBlock(buffer.Span);
}
}
-
- /// <summary>
- /// Hashes a specified password, with the initialized pepper, and salted with a CNG random bytes.
- /// </summary>
- /// <param name="password">Password to be hashed</param>
+
+ /// <inheritdoc/>
/// <exception cref="VnArgon2Exception"></exception>
/// <returns>A <see cref="PrivateString"/> of the hashed and encoded password</returns>
public PrivateString Hash(ReadOnlySpan<byte> password)
@@ -231,5 +217,76 @@ namespace VNLib.Plugins.Essentials.Accounts
MemoryUtil.InitializeBlock(secretBuffer.Span);
}
}
+
+ /// <summary>
+ /// NOT SUPPORTED! Use <see cref="Verify(ReadOnlySpan{byte}, ReadOnlySpan{byte}, ReadOnlySpan{byte})"/>
+ /// instead to specify the salt that was used to encypt the original password
+ /// </summary>
+ /// <param name="passHash"></param>
+ /// <param name="password"></param>
+ /// <exception cref="NotSupportedException"></exception>
+ public bool Verify(ReadOnlySpan<byte> passHash, ReadOnlySpan<byte> password)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///<inheritdoc/>
+ ///<exception cref="VnArgon2Exception"></exception>
+ public ERRNO Hash(ReadOnlySpan<byte> password, Span<byte> hashOutput)
+ {
+ //Calc the min buffer size
+ int minBufferSize = SaltLen + _secret.BufferSize + (int)HashLen;
+
+ //Alloc heap buffer
+ using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAllocNearestPage<byte>(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<byte> SaltBuffer;
+
+ public readonly Span<byte> SecretBuffer;
+
+ public readonly Span<byte> HashBuffer;
+
+ public HashBufferSegments(Span<byte> 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
/// </summary>
public abstract class ProtectedWebEndpoint : UnprotectedWebEndpoint
{
+ /// <summary>
+ /// Gets the minium <see cref="AuthorzationCheckLevel"/> required by a client to
+ /// access this endpoint
+ /// </summary>
+ protected virtual AuthorzationCheckLevel AuthLevel { get; } = AuthorzationCheckLevel.Critical;
+
///<inheritdoc/>
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
{
+
/// <summary>
/// Provides an abstract base implementation of <see cref="IWebRoot"/>
/// that breaks down simple processing procedures, routing, and session
/// loading.
/// </summary>
- public abstract class EventProcessor : IWebRoot
+ public abstract class EventProcessor : IWebRoot, IWebProcessor
{
private static readonly AsyncLocal<EventProcessor?> _currentProcessor = new();
@@ -70,6 +72,9 @@ namespace VNLib.Plugins.Essentials
/// </summary>
public abstract IEpProcessingOptions Options { get; }
+ ///<inheritdoc/>
+ public abstract IReadOnlyDictionary<string, Redirect> Redirects { get; }
+
/// <summary>
/// Event log provider
/// </summary>
@@ -112,23 +117,10 @@ namespace VNLib.Plugins.Essentials
/// <param name="chosenRoutine">The selected file processing routine for the given request</param>
public abstract void PostProcessFile(HttpEntity entity, in FileProcessArgs chosenRoutine);
- #region redirects
- ///<inheritdoc/>
- public IReadOnlyDictionary<string, Redirect> Redirects => _redirects;
-
- private Dictionary<string, Redirect> _redirects = new();
+ #region security
- /// <summary>
- /// Initializes 301 redirects table from a collection of redirects
- /// </summary>
- /// <param name="redirs">A collection of redirects</param>
- public void SetRedirects(IEnumerable<Redirect> redirs)
- {
- //To dictionary
- Dictionary<string, Redirect> r = redirs.ToDictionary(r => r.Url, r => r, StringComparer.OrdinalIgnoreCase);
- //Swap
- _ = Interlocked.Exchange(ref _redirects, r);
- }
+ ///<inheritdoc/>
+ 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
/// </summary>
- private Dictionary<string, IVirtualEndpoint<HttpEntity>> VirtualEndpoints = new();
+ private IReadOnlyDictionary<string, IVirtualEndpoint<HttpEntity>> VirtualEndpoints = new Dictionary<string, IVirtualEndpoint<HttpEntity>>();
/*
@@ -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());
}
/// <summary>
@@ -281,9 +273,10 @@ namespace VNLib.Plugins.Essentials
/// <exception cref="ArgumentException"></exception>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InvalidOperationException"></exception>
- 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
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="ContentTypeUnacceptableException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, in ReadOnlySpan<char> data)
+ public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan<char> 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);
}
/// <summary>
@@ -338,7 +338,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="encoding">The encoding type to use when converting the buffer</param>
/// <remarks>This method will store an encoded copy as a memory stream, so be careful with large buffers</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, in ReadOnlySpan<char> data, Encoding encoding)
+ public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan<char> data, Encoding encoding)
{
if (data.IsEmpty)
{
@@ -479,7 +479,7 @@ namespace VNLib.Plugins.Essentials.Extensions
try
{
//Deserialize and return the object
- obj = value.AsJsonObject<T>(options);
+ obj = JsonSerializer.Deserialize<T>(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<T>(file.FileData, options);
+ return JsonSerializer.Deserialize<T>(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
/// <param name="Value">The cookie value</param>
public readonly record struct HttpCookie (string Name, string Value)
{
+ /// <summary>
+ /// The length of time the cookie is valid for
+ /// </summary>
public readonly TimeSpan ValidFor { get; init; } = TimeSpan.MaxValue;
- public readonly string Domain { get; init; } = "";
- public readonly string Path { get; init; } = "/";
+ /// <summary>
+ /// The cookie's domain parameter. If null, is not set in the
+ /// Set-Cookie header.
+ /// </summary>
+ public readonly string? Domain { get; init; } = null;
+ /// <summary>
+ /// The cookies path parameter. If null, is not
+ /// set in the Set-Cookie header.
+ /// </summary>
+ public readonly string? Path { get; init; } = "/";
+ /// <summary>
+ /// The cookie's same-site parameter. Default is <see cref="CookieSameSite.None"/>
+ /// </summary>
public readonly CookieSameSite SameSite { get; init; } = CookieSameSite.None;
+ /// <summary>
+ /// Sets the cookie's HttpOnly parameter. Default is false. When false, does not
+ /// set the HttpOnly paramter in the Set-Cookie header.
+ /// </summary>
public readonly bool HttpOnly { get; init; } = false;
+ /// <summary>
+ /// Sets the cookie's Secure parameter. Default is false. When false, does not
+ /// set the Secure parameter in the Set-Cookie header.
+ /// </summary>
public readonly bool Secure { get; init; } = false;
/// <summary>
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
/// <summary>
/// The requested web root. Provides additional site information
/// </summary>
- public readonly EventProcessor RequestedRoot;
+ public readonly IWebProcessor RequestedRoot;
/// <summary>
/// If the request has query arguments they are stored in key value format
/// </summary>
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 <see cref="TimeSpan"/> for how long a connection may remain open before all operations are cancelled
/// </summary>
TimeSpan ExecutionTimeout { get; }
+ /// <summary>
+ /// HTTP level "hard" 301 redirects
+ /// </summary>
+ IReadOnlyDictionary<string, Redirect> 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
+{
+ /// <summary>
+ /// Abstractions for methods and information for processors
+ /// </summary>
+ public interface IWebProcessor : IWebRoot
+ {
+ /// <summary>
+ /// The filesystem entrypoint path for the site
+ /// </summary>
+ string Directory { get; }
+
+ /// <summary>
+ /// Gets the EP processing options
+ /// </summary>
+ IEpProcessingOptions Options { get; }
+
+ /// <summary>
+ /// The shared <see cref="IAccountSecurityProvider"/> that provides
+ /// user account security operations
+ /// </summary>
+ IAccountSecurityProvider AccountSecurity { get; }
+
+ /// <summary>
+ /// <para>
+ /// Called when the server intends to process a file and requires translation from a
+ /// uri path to a usable filesystem path
+ /// </para>
+ /// <para>
+ /// NOTE: This function must be thread-safe!
+ /// </para>
+ /// </summary>
+ /// <param name="requestPath">The path requested by the request </param>
+ /// <returns>The translated and filtered filesystem path used to identify the file resource</returns>
+ string TranslateResourcePath(string requestPath);
+
+ /// <summary>
+ /// 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
+ /// </summary>
+ bool FindResourceInRoot(string resourcePath, bool fullyQualified, out string path);
+
+ /// <summary>
+ /// Determines if a requested resource exists within the <see cref="EventProcessor"/> and is allowed to be accessed.
+ /// </summary>
+ /// <param name="resourcePath">The path to the resource</param>
+ /// <param name="path">An out parameter that is set to the absolute path to the existing and accessable resource</param>
+ /// <returns>True if the resource exists and is allowed to be accessed</returns>
+ 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<StringBuilder> SbRental { get; } = ObjectRental.CreateThreadLocal(Constructor, null, ReturnFunc);
-
- private static StringBuilder Constructor() => new(64);
- private static void ReturnFunc(StringBuilder sb) => sb.Clear();
-
/// <summary>
/// Closes the current response with a json error message with the message details
/// </summary>
@@ -86,134 +80,53 @@ namespace VNLib.Plugins.Essentials.Oauth
/// <param name="code">The http status code</param>
/// <param name="error">The short error</param>
/// <param name="description">The error description message</param>
- 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<char> 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);
- }
- }
- /// <summary>
- /// Closes the current response with a json error message with the message details
- /// </summary>
- /// <param name="ev"></param>
- /// <param name="code">The http status code</param>
- /// <param name="error">The short error</param>
- /// <param name="description">The error description message</param>
- 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<char> buffer = MemoryUtil.UnsafeAllocNearestPage<char>(description.Length + 64);
+ ForwardOnlyWriter<char> 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 <see cref="ISession"/> interface for exclusive use within a multithreaded
/// context
/// </summary>
- public abstract class SessionBase : AsyncExclusiveResource<IHttpEvent>, 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
}
///<inheritdoc/>
- public virtual string SessionID { get; protected set; }
+ public abstract string SessionID { get; }
///<inheritdoc/>
- public virtual DateTimeOffset Created { get; protected set; }
+ public abstract DateTimeOffset Created { get; set; }
///<inheritdoc/>
///<exception cref="ObjectDisposedException"></exception>
public string this[string index]
{
- get
- {
- Check();
- return IndexerGet(index);
- }
- set
- {
- Check();
- IndexerSet(index, value);
- }
+ get => IndexerGet(index);
+ set => IndexerSet(index, value);
}
///<inheritdoc/>
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
{
/// <summary>
@@ -55,13 +58,46 @@ namespace VNLib.Plugins.Essentials.Sessions
/// </remarks>
public readonly struct SessionInfo : IObjectStorage, IEquatable<SessionInfo>
{
+ /*
+ * 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;
+
/// <summary>
/// A value indicating if the current instance has been initiailzed
/// with a session. Otherwise properties are undefied
/// </summary>
- public readonly bool IsSet;
+ public readonly bool IsSet
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _flags.HasFlag(SessionFlags.IsSet);
+ }
- private readonly ISession UserSession;
+ /// <summary>
+ /// The origin header specified during session creation
+ /// </summary>
+ public readonly Uri? SpecifiedOrigin;
+
+ /// <summary>
+ /// Was the session Initialy established on a secure connection?
+ /// </summary>
+ public readonly SslProtocols SecurityProcol;
+
+ /// <summary>
+ /// Session stored User-Agent
+ /// </summary>
+ public readonly string? UserAgent;
/// <summary>
/// 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;
}
- /// <summary>
- /// Session stored User-Agent
- /// </summary>
- public readonly string UserAgent;
+
/// <summary>
/// If the stored IP and current user's IP matches
/// </summary>
- public readonly bool IPMatch;
+ public readonly bool IPMatch
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _flags.HasFlag(SessionFlags.IpMatch);
+ }
+
/// <summary>
/// If the current connection and stored session have matching cross origin domains
/// </summary>
- public readonly bool CrossOriginMatch;
- /// <summary>
- /// Flags the session as invalid. IMPORTANT: the user's session data is no longer valid and will throw an exception when accessed
- /// </summary>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void Invalidate(bool all = false) => UserSession.Invalidate(all);
- /// <summary>
- /// Marks the session ID to be regenerated during closing event
- /// </summary>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void RegenID() => UserSession.RegenID();
- ///<inheritdoc/>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public T GetObject<T>(string key) => this[key].AsJsonObject<T>(SR_OPTIONS);
- ///<inheritdoc/>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void SetObject<T>(string key, T obj) => this[key] = obj?.ToJsonString(SR_OPTIONS);
+ public readonly bool CrossOriginMatch
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _flags.HasFlag(SessionFlags.CrossOriginMatch);
+ }
/// <summary>
/// Was the original session cross origin?
/// </summary>
- public readonly bool CrossOrigin;
+ public readonly bool CrossOrigin
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _flags.HasFlag(SessionFlags.IsCrossOrigin);
+ }
+
/// <summary>
- /// The origin header specified during session creation
+ /// Was this session just created on this connection?
/// </summary>
- public readonly Uri SpecifiedOrigin;
+ public readonly bool IsNew
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => UserSession.IsNew;
+ }
+
/// <summary>
/// The time the session was created
/// </summary>
- public readonly DateTimeOffset Created;
- /// <summary>
- /// Was this session just created on this connection?
- /// </summary>
- public readonly bool IsNew;
+ public readonly DateTimeOffset Created
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => UserSession.Created;
+ }
+
/// <summary>
/// Gets or sets the session's login hash, if set to a non-empty/null value, will trigger an upgrade on close
/// </summary>
@@ -126,6 +163,7 @@ namespace VNLib.Plugins.Essentials.Sessions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => UserSession.SetLoginToken(value);
}
+
/// <summary>
/// Gets or sets the session's login token, if set to a non-empty/null value, will trigger an upgrade on close
/// </summary>
@@ -136,6 +174,7 @@ namespace VNLib.Plugins.Essentials.Sessions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => UserSession.Token = value;
}
+
/// <summary>
/// <para>
/// 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;
}
+
/// <summary>
/// Privilages associated with user specified during login
/// </summary>
@@ -161,6 +201,7 @@ namespace VNLib.Plugins.Essentials.Sessions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => UserSession.Privilages = value;
}
+
/// <summary>
/// The IP address belonging to the client
/// </summary>
@@ -168,11 +209,8 @@ namespace VNLib.Plugins.Essentials.Sessions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => UserSession.UserIP;
- }
- /// <summary>
- /// Was the session Initialy established on a secure connection?
- /// </summary>
- public readonly SslProtocols SecurityProcol;
+ }
+
/// <summary>
/// A value specifying the type of the backing session
/// </summary>
@@ -183,6 +221,27 @@ namespace VNLib.Plugins.Essentials.Sessions
}
/// <summary>
+ /// 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
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Invalidate(bool all = false) => UserSession.Invalidate(all);
+ /// <summary>
+ /// Marks the session ID to be regenerated during closing event
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void RegenID() => UserSession.RegenID();
+
+#nullable disable
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public T GetObject<T>(string key) => JsonSerializer.Deserialize<T>(this[key], SR_OPTIONS);
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetObject<T>(string key, T obj) => this[key] = obj == null ? null: JsonSerializer.Serialize(obj, SR_OPTIONS);
+#nullable enable
+
+ /// <summary>
/// Accesses the session's general storage
/// </summary>
/// <param name="index">Key for specifie data</param>
@@ -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;
}
///<inheritdoc/>
public bool Equals(SessionInfo other) => SessionID.Equals(other.SessionID, StringComparison.Ordinal);
///<inheritdoc/>
- public override bool Equals(object obj) => obj is SessionInfo si && Equals(si);
+ public override bool Equals(object? obj) => obj is SessionInfo si && Equals(si);
///<inheritdoc/>
public override int GetHashCode() => SessionID.GetHashCode(StringComparison.Ordinal);
///<inheritdoc/>
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
+{
+ /// <summary>
+ /// Represents a plugin event consumer.
+ /// </summary>
+ public interface IPluginEventListener
+ {
+ /// <summary>
+ /// Called by the registered <see cref="PluginController"/>
+ /// to notify this listener that the plugins within the collection
+ /// have been initialized and loaded
+ /// </summary>
+ /// <param name="controller">The collection on which the load event occured</param>
+ /// <param name="state">The registration state parameter</param>
+ void OnPluginLoaded(PluginController controller, object? state);
+ /// <summary>
+ /// Called by the registered <see cref="PluginController"/>
+ /// to notify this listener that this plugins within the
+ /// collection have been unloaded
+ /// </summary>
+ /// <param name="controller">The controller that is reloading</param>
+ /// <param name="state">The registration state parameter</param>
+ 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
+{
+ /// <summary>
+ /// Represents a type that accepts <see cref="IPluginEventListener"/>
+ /// event handlers and allow them to unload events
+ /// </summary>
+ public interface IPluginEventRegistrar
+ {
+ /// <summary>
+ /// Registers a plugin event listener
+ /// </summary>
+ /// <param name="listener">The event handler instance to register</param>
+ /// <param name="state">An optional state paremeter to pass to the event handler</param>
+ void Register(IPluginEventListener listener, object? state = null);
+
+ /// <summary>
+ /// Unregisters the event listener
+ /// </summary>
+ /// <param name="listener">The event handler instance to unregister</param>
+ /// <returns>A value that indicates if the event handler was successfully unregistered</returns>
+ 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
+{
+ /// <summary>
+ /// An abstraction that represents a consumer of a dynamically loaded type.
+ /// </summary>
+ /// <typeparam name="T">The service type to consume</typeparam>
+ public interface ITypedPluginConsumer<T>
+ {
+ /// <summary>
+ /// Invoked when the instance of the desired type is loaded.
+ /// This is a new instance of the desired type
+ /// </summary>
+ /// <param name="plugin">A new instance of the requested type</param>
+ void OnLoad(T plugin, object? state);
+
+ /// <summary>
+ /// Called when the loader that maintains the instance is unloading
+ /// the type.
+ /// </summary>
+ /// <param name="plugin">The instance of the type that is being unloaded</param>
+ 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
{
+
/// <summary>
/// <para>
/// Wrapper for a loaded <see cref="IPlugin"/> instance, used internally
@@ -46,6 +46,8 @@ namespace VNLib.Plugins.Runtime
/// </summary>
public class LivePlugin : IEquatable<IPlugin>, IEquatable<LivePlugin>
{
+ private bool _loaded;
+
/// <summary>
/// The plugin's <see cref="IPlugin.PluginName"/> property during load time
/// </summary>
@@ -57,14 +59,24 @@ namespace VNLib.Plugins.Runtime
/// by he current instance
/// </summary>
public IPlugin? Plugin { get; private set; }
+
+ /// <summary>
+ /// The assembly that this plugin was created from
+ /// </summary>
+ public Assembly OriginAsm { get; }
- private readonly Type PluginType;
+ /// <summary>
+ /// The exposed runtime type of the plugin. Equivalent to
+ /// calling <code>Plugin.GetType()</code>
+ /// </summary>
+ 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
/// <summary>
/// Calls the <see cref="IPlugin.Load"/> method on the plugin if its loaded
/// </summary>
- internal void LoadPlugin() => Plugin?.Load();
+ internal void LoadPlugin()
+ {
+ //Load and set loaded flag
+ Plugin?.Load();
+ _loaded = true;
+ }
/// <summary>
- /// 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 <see cref="IPlugin.Unload"/> event hook.
/// </summary>
- /// <param name="logSink">An optional log provider to write unload exceptions to</param>
- /// <remarks>
- /// If <paramref name="logSink"/> is no null unload exceptions are swallowed and written to the log
- /// </remarks>
- 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;
}
+
///<inheritdoc/>
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
{
+ /// <summary>
+ /// Contains extension methods for PluginLoader library
+ /// </summary>
public static class LoaderExtensions
{
- /// <summary>
- /// Searches all plugins within the current loader for a
- /// single plugin that derrives the specified type
- /// </summary>
- /// <typeparam name="T">The type the plugin must derrive from</typeparam>
- /// <param name="loader"></param>
- /// <returns>The instance of the plugin that derrives from the specified type</returns>
- public static LivePlugin? GetExposedPlugin<T>(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<T> : IPluginEventListener where T: class
{
- return loader.LivePlugins
- .Where(static pl => typeof(T).IsAssignableFrom(pl.Plugin!.GetType()))
- .SingleOrDefault();
+ private readonly ITypedPluginConsumer<T> _consumerEvents;
+ private readonly object? _userState;
+
+ private T? _service;
+ private readonly Type _type;
+
+ public TypedRegistration(ITypedPluginConsumer<T> 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;
+ }
}
/// <summary>
- /// Searches all plugins within the current loader for a
- /// single plugin that derrives the specified type
+ /// Registers a plugin even handler for the current <see cref="PluginController"/>
+ /// for a specific type.
/// </summary>
- /// <typeparam name="T">The type the plugin must derrive from</typeparam>
- /// <param name="loader"></param>
- /// <returns>The instance of your custom type casted, or null if not found or could not be casted</returns>
- public static T? GetExposedTypeFromPlugin<T>(this RuntimePluginLoader loader) where T: class
+ /// <typeparam name="T"></typeparam>
+ /// <param name="collection"></param>
+ /// <param name="consumer">The typed plugin instance event consumer</param>
+ /// <returns>A <see cref="PluginEventRegistration"/> handle that manages this event registration</returns>
+ /// <exception cref="ArgumentException"></exception>
+ public static PluginEventRegistration RegisterForType<T>(this PluginController collection, ITypedPluginConsumer<T> 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<T> reg = new(consumer, serviceType);
+
+ //register event handler
+ return Register(collection, reg, null);
}
/// <summary>
- /// Registers a listener delegate method to invoke when the
- /// current <see cref="RuntimePluginLoader"/> is reloaded, and passes
- /// the new instance of the specified type
+ /// Registers a handler to listen for plugin load/unload events
/// </summary>
- /// <typeparam name="T">The single plugin type to register a listener for</typeparam>
- /// <param name="loader"></param>
- /// <param name="reloaded">The delegate method to invoke when the loader has reloaded plugins</param>
/// <exception cref="ArgumentNullException"></exception>
- public static bool RegisterListenerForSingle<T>(this RuntimePluginLoader loader, Action<T, T> reloaded) where T: class
+ /// <returns>A <see cref="PluginEventRegistration"/> handle that will unregister the listener when disposed</returns>
+ 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<T>();
+ /// <summary>
+ /// Loads the configuration file into its <see cref="JsonDocument"/> format
+ /// for reading.
+ /// </summary>
+ /// <param name="loader"></param>
+ /// <returns>A new <see cref="JsonDocument"/> of the loaded configuration file</returns>
+ public static async Task<JsonDocument> 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<T>()!.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);
}
/// <summary>
- /// Gets all endpoints exposed by all exported plugin instances
- /// within the current loader
+ /// Determines if the current <see cref="PluginController"/>
+ /// exposes the desired type on is <see cref="IPlugin"/>
+ /// type.
/// </summary>
- /// <param name="loader"></param>
- /// <returns>An enumeration of all endpoints</returns>
- public static IEnumerable<IEndpoint> GetEndpoints(this RuntimePluginLoader loader) => loader.LivePlugins.SelectMany(static pl => pl.Plugin!.GetEndpoints());
+ /// <param name="collection"></param>
+ /// <param name="type">The desired type to request</param>
+ /// <returns>True if the plugin exposes the desired type, false otherwise</returns>
+ public static bool ExposesType(this PluginController collection, Type type)
+ {
+ return collection.Plugins
+ .Where(pl => type.IsAssignableFrom(pl.PluginType))
+ .Any();
+ }
/// <summary>
- /// 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
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type the plugin must derrive from</typeparam>
/// <param name="loader"></param>
- /// <returns>True if any plugin instance exposes a the specified type, false otherwise</returns>
- public static bool ExposesType<T>(this RuntimePluginLoader loader) where T : class
+ /// <returns>The instance of your custom type casted, or null if not found or could not be casted</returns>
+ public static T? GetExposedTypes<T>(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
+{
+ /// <summary>
+ /// Manages the lifetime of a collection of <see cref="IPlugin"/> instances,
+ /// and their dependent event listeners
+ /// </summary>
+ public sealed class PluginController : IPluginEventRegistrar
+ {
+ private readonly List<LivePlugin> _plugins;
+ private readonly List<KeyValuePair<IPluginEventListener, object?>> _listeners;
+
+ internal PluginController()
+ {
+ _plugins = new ();
+ _listeners = new ();
+ }
+
+ /// <summary>
+ /// The current collection of plugins. Valid before the unload event.
+ /// </summary>
+ public IEnumerable<LivePlugin> Plugins => _plugins;
+
+ ///<inheritdoc/>
+ ///<exception cref="ArgumentNullException"></exception>
+ public void Register(IPluginEventListener listener, object? state = null)
+ {
+ _ = listener ?? throw new ArgumentNullException(nameof(listener));
+
+ _listeners.Add(new(listener, state));
+ }
+
+ ///<inheritdoc/>
+ 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
+{
+ /// <summary>
+ /// Holds a registration for events to a given <see cref="IPluginEventRegistrar"/>.
+ /// The event listener is unregistered from events when this registration is disposed.
+ /// </summary>
+ public readonly record struct PluginEventRegistration : IDisposable
+ {
+ private readonly IPluginEventRegistrar _registrar;
+ private readonly IPluginEventListener _listener;
+
+ internal PluginEventRegistration(IPluginEventRegistrar container, IPluginEventListener listener)
+ {
+ _listener = listener;
+ _registrar = container;
+ }
+
+ /// <summary>
+ /// Unreigsers the listner and releases held resources
+ /// </summary>
+ public readonly void Dispose()
+ {
+ _ = _registrar?.Unregister(_listener);
+ }
+
+ /// <summary>
+ /// Unregisters a previously registered <see cref="IPluginEventListener"/>
+ /// from the <see cref="PluginController"/> it was registered to
+ /// </summary>
+ /// <exception cref="InvalidOperationException"></exception>
+ 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 <see cref="IPlugin"/> types.
/// </summary>
- 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<LivePlugin> LoadedPlugins;
-
- /// <summary>
- /// A readonly collection of all loaded plugin wrappers
- /// </summary>
- public IReadOnlyCollection<LivePlugin> LivePlugins => LoadedPlugins;
-
- /// <summary>
- /// An event that is raised before the loader
- /// unloads all plugin instances
- /// </summary>
- protected event EventHandler<PluginReloadedEventArgs>? OnBeforeReloaded;
- /// <summary>
- /// An event that is raised after a successfull reload of all new
- /// plugins for the instance
- /// </summary>
- protected event EventHandler? OnAfterReloaded;
-
- /// <summary>
- /// Raised when the current loader has reloaded the assembly and
- /// all plugins were successfully loaded.
- /// </summary>
- public event EventHandler? Reloaded;
+ private readonly PluginLoader Loader;
+ private readonly string PluginPath;
+ private readonly JsonDocument HostConfig;
+ private readonly ILogProvider? Log;
/// <summary>
- /// 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.
/// </summary>
- public JsonDocument? PluginConfigDOM { get; private set; }
- /// <summary>
- /// Optional loader arguments object for the plugin
- /// </summary>
- protected JsonElement? LoaderArgs { get; private set; }
+ public PluginController Controller { get; }
/// <summary>
/// The path of the plugin's configuration file. (Default = pluginPath.json)
/// </summary>
- public string PluginConfigPath { get; init; }
+ public string PluginConfigPath { get; }
+
/// <summary>
/// Creates a new <see cref="RuntimePluginLoader"/> with the specified
/// assembly location and host config.
@@ -98,18 +67,16 @@ namespace VNLib.Plugins.Runtime
/// <param name="hostConfig">The configuration DOM to merge with plugin config DOM and pass to enabled plugins</param>
/// <param name="unloadable">A value that specifies if the assembly can be unloaded</param>
/// <param name="hotReload">A value that spcifies if the loader will listen for changes to the assembly file and reload the plugins</param>
- /// <param name="lazy">A value that specifies if assembly dependencies are loaded on-demand</param>
/// <remarks>
/// The <paramref name="log"/> argument may be null if <paramref name="unloadable"/> is false
/// </remarks>
/// <exception cref="ArgumentNullException"></exception>
- 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)
{
}
+
/// <summary>
/// Creates a new <see cref="RuntimePluginLoader"/> with the specified config and host config dom.
/// </summary>
@@ -126,124 +94,119 @@ namespace VNLib.Plugins.Runtime
/// <exception cref="ArgumentNullException"></exception>
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);
}
}
/// <summary>
- /// Initializes the plugin loader, the assembly, and all public <see cref="IPlugin"/>
- /// types
+ /// Initializes the plugin loader, and populates the <see cref="Controller"/>
+ /// with initialized plugins.
/// </summary>
/// <returns>A task that represents the initialization</returns>
- public async Task InitLoaderAsync()
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="FileNotFoundException"></exception>
+ 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<IPlugin> 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();
}
}
+
+ /// <summary>
+ /// Loads all configured plugins by calling <see cref="IPlugin.Load"/>
+ /// event hook on the current thread. Loading exceptions are aggregated so not
+ /// to block individual loading.
+ /// </summary>
+ /// <exception cref="AggregateException"></exception>
+ public void LoadPlugins()
+ {
+ //Load all plugins
+ Controller.LoadPlugins();
+ }
+
/// <summary>
/// Manually reload the internal <see cref="PluginLoader"/>
- /// which will reload the assembly and its plugins and endpoints
+ /// which will reload the assembly and its plugins
/// </summary>
- public void ReloadPlugin() => Loader.Reload();
+ public void ReloadPlugins() => Loader.Reload();
/// <summary>
/// Attempts to unload all plugins.
/// </summary>
/// <exception cref="AggregateException"></exception>
- public void UnloadAll() => LoadedPlugins.TryForeach(lp => lp.UnloadPlugin(Log));
+ public void UnloadAll() => Controller.UnloadPlugins();
///<inheritdoc/>
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 @@
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>VNLib.Plugins.Runtime</RootNamespace>
<AssemblyName>VNLib.Plugins.Runtime</AssemblyName>
- <Version>1.0.1.1</Version>
<AnalysisLevel>latest-all</AnalysisLevel>
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
@@ -44,7 +43,7 @@
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\..\Plugins.Essentials\src\VNLib.Plugins.Essentials.csproj" />
+ <ProjectReference Include="..\..\Plugins\src\VNLib.Plugins.csproj" />
<ProjectReference Include="..\..\Utils\src\VNLib.Utils.csproj" />
</ItemGroup>
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
@@ -64,19 +64,6 @@ namespace VNLib.Utils.Extensions
public static class JsonExtensions
{
/// <summary>
- /// Converts a JSON encoded string to an object of the specified type
- /// </summary>
- /// <typeparam name="T">Output type of the object</typeparam>
- /// <param name="value"></param>
- /// <param name="options"><see cref="JsonSerializerOptions"/> to use during de-serialization</param>
- /// <returns>The new object or default if the string is null or empty</returns>
- /// <exception cref="JsonException"></exception>
- /// <exception cref="NotSupportedException"></exception>
- public static T? AsJsonObject<T>(this string value, JsonSerializerOptions? options = null)
- {
- return !string.IsNullOrWhiteSpace(value) ? JsonSerializer.Deserialize<T>(value, options) : default;
- }
- /// <summary>
/// Converts a JSON encoded binary data to an object of the specified type
/// </summary>
/// <typeparam name="T">Output type of the object</typeparam>
@@ -159,18 +146,6 @@ namespace VNLib.Utils.Extensions
}
/// <summary>
- /// Attemts to serialze an object to a JSON encoded string
- /// </summary>
- /// <param name="obj"></param>
- /// <param name="options"><see cref="JsonSerializerOptions"/> to use during serialization</param>
- /// <returns>A JSON encoded string of the serialized object, or null if the object is null</returns>
- /// <exception cref="NotSupportedException"></exception>
- public static string? ToJsonString<T>(this T obj, JsonSerializerOptions? options = null)
- {
- return obj == null ? null : JsonSerializer.Serialize(obj, options);
- }
-
- /// <summary>
/// Merges the current <see cref="JsonDocument"/> with another <see cref="JsonDocument"/> to
/// create a new document of combined properties
/// </summary>
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;
}
}
-
- /// <summary>
- /// Attempts to deserialze a json object from a stream of UTF8 data
- /// </summary>
- /// <typeparam name="T">The type of the object to deserialize</typeparam>
- /// <param name="data">Binary data to read from</param>
- /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param>
- /// <returns>The object decoded from the stream</returns>
- /// <exception cref="JsonException"></exception>
- /// <exception cref="NotSupportedException"></exception>
- public static T? JSONDeserializeFromBinary<T>(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<T>(options);
- }
- //Stream is empty
- return default;
- }
- /// <summary>
- /// Attempts to deserialze a json object from a stream of UTF8 data
- /// </summary>
- /// <param name="data">Binary data to read from</param>
- /// <param name="type"></param>
- /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param>
- /// <returns>The object decoded from the stream</returns>
- /// <exception cref="JsonException"></exception>
- /// <exception cref="NotSupportedException"></exception>
- 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;
- }
+
/// <summary>
/// Attempts to deserialze a json object from a stream of UTF8 data
/// </summary>
@@ -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('=');
}
}