aboutsummaryrefslogtreecommitdiff
path: root/Plugins.Essentials/src
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-01-08 14:44:01 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-01-08 14:44:01 -0500
commitbe6dc557a3b819248b014992eb96c1cb21f8112b (patch)
tree5361530552856ba8154bfcfbfac8377549117c9e /Plugins.Essentials/src
parent072a1294646542a73007784d08a35ffcad557b1b (diff)
Initial commit
Diffstat (limited to 'Plugins.Essentials/src')
-rw-r--r--Plugins.Essentials/src/Accounts/AccountData.cs52
-rw-r--r--Plugins.Essentials/src/Accounts/AccountManager.cs872
-rw-r--r--Plugins.Essentials/src/Accounts/INonce.cs90
-rw-r--r--Plugins.Essentials/src/Accounts/LoginMessage.cs102
-rw-r--r--Plugins.Essentials/src/Accounts/PasswordHashing.cs244
-rw-r--r--Plugins.Essentials/src/Content/IPageRouter.cs43
-rw-r--r--Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs58
-rw-r--r--Plugins.Essentials/src/Endpoints/ProtectionSettings.cs103
-rw-r--r--Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs346
-rw-r--r--Plugins.Essentials/src/Endpoints/UnprotectedWebEndpoint.cs42
-rw-r--r--Plugins.Essentials/src/Endpoints/VirtualEndpoint.cs67
-rw-r--r--Plugins.Essentials/src/EventProcessor.cs728
-rw-r--r--Plugins.Essentials/src/Extensions/CollectionsExtensions.cs85
-rw-r--r--Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs361
-rw-r--r--Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs848
-rw-r--r--Plugins.Essentials/src/Extensions/IJsonSerializerBuffer.cs48
-rw-r--r--Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs100
-rw-r--r--Plugins.Essentials/src/Extensions/InvalidJsonRequestException.cs57
-rw-r--r--Plugins.Essentials/src/Extensions/JsonResponse.cs112
-rw-r--r--Plugins.Essentials/src/Extensions/RedirectType.cs37
-rw-r--r--Plugins.Essentials/src/Extensions/SimpleMemoryResponse.cs89
-rw-r--r--Plugins.Essentials/src/Extensions/UserExtensions.cs94
-rw-r--r--Plugins.Essentials/src/FileProcessArgs.cs169
-rw-r--r--Plugins.Essentials/src/HttpEntity.cs178
-rw-r--r--Plugins.Essentials/src/IEpProcessingOptions.cs66
-rw-r--r--Plugins.Essentials/src/Oauth/IOAuth2Provider.cs44
-rw-r--r--Plugins.Essentials/src/Oauth/O2EndpointBase.cs162
-rw-r--r--Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs239
-rw-r--r--Plugins.Essentials/src/Oauth/OauthSessionCacheExhaustedException.cs43
-rw-r--r--Plugins.Essentials/src/Oauth/OauthSessionExtensions.cs88
-rw-r--r--Plugins.Essentials/src/Sessions/ISession.cs94
-rw-r--r--Plugins.Essentials/src/Sessions/ISessionExtensions.cs95
-rw-r--r--Plugins.Essentials/src/Sessions/ISessionProvider.cs49
-rw-r--r--Plugins.Essentials/src/Sessions/SessionBase.cs168
-rw-r--r--Plugins.Essentials/src/Sessions/SessionCacheLimitException.cs41
-rw-r--r--Plugins.Essentials/src/Sessions/SessionException.cs48
-rw-r--r--Plugins.Essentials/src/Sessions/SessionHandle.cs123
-rw-r--r--Plugins.Essentials/src/Sessions/SessionInfo.cs231
-rw-r--r--Plugins.Essentials/src/Statics.cs44
-rw-r--r--Plugins.Essentials/src/TimestampedCounter.cs117
-rw-r--r--Plugins.Essentials/src/Users/IUser.cs74
-rw-r--r--Plugins.Essentials/src/Users/IUserManager.cs103
-rw-r--r--Plugins.Essentials/src/Users/UserCreationFailedException.cs47
-rw-r--r--Plugins.Essentials/src/Users/UserDeleteException.cs44
-rw-r--r--Plugins.Essentials/src/Users/UserExistsException.cs49
-rw-r--r--Plugins.Essentials/src/Users/UserStatus.cs50
-rw-r--r--Plugins.Essentials/src/Users/UserUpdateException.cs43
-rw-r--r--Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj53
-rw-r--r--Plugins.Essentials/src/WebSocketSession.cs204
49 files changed, 7244 insertions, 0 deletions
diff --git a/Plugins.Essentials/src/Accounts/AccountData.cs b/Plugins.Essentials/src/Accounts/AccountData.cs
new file mode 100644
index 0000000..d4a4d12
--- /dev/null
+++ b/Plugins.Essentials/src/Accounts/AccountData.cs
@@ -0,0 +1,52 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: AccountData.cs
+*
+* AccountData.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.Text.Json.Serialization;
+
+namespace VNLib.Plugins.Essentials.Accounts
+{
+ public class AccountData
+ {
+ [JsonPropertyName("email")]
+ public string EmailAddress { get; set; }
+ [JsonPropertyName("phone")]
+ public string PhoneNumber { get; set; }
+ [JsonPropertyName("first")]
+ public string First { get; set; }
+ [JsonPropertyName("last")]
+ public string Last { get; set; }
+ [JsonPropertyName("company")]
+ public string Company { get; set; }
+ [JsonPropertyName("street")]
+ public string Street { get; set; }
+ [JsonPropertyName("city")]
+ public string City { get; set; }
+ [JsonPropertyName("state")]
+ public string State { get; set; }
+ [JsonPropertyName("zip")]
+ public string Zip { get; set; }
+ [JsonPropertyName("created")]
+ public string Created { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Accounts/AccountManager.cs b/Plugins.Essentials/src/Accounts/AccountManager.cs
new file mode 100644
index 0000000..f148fdb
--- /dev/null
+++ b/Plugins.Essentials/src/Accounts/AccountManager.cs
@@ -0,0 +1,872 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: AccountManager.cs
+*
+* AccountManager.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 System.IO;
+using System.Text;
+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
+
+namespace VNLib.Plugins.Essentials.Accounts
+{
+
+ /// <summary>
+ /// Provides essential constants, static methods, and session/user extensions
+ /// to facilitate unified user-controls, athentication, and security
+ /// application-wide
+ /// </summary>
+ public static partial class AccountManager
+ {
+ 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 CHALLENGE_HASH_ENTRY = "acnt.chl";
+
+ //Privlage masks
+ public const ulong READ_MSK = 0x0000000000000001L;
+ public const ulong DOWNLOAD_MSK = 0x0000000000000002L;
+ public const ulong WRITE_MSK = 0x0000000000000004L;
+ public const ulong DELETE_MSK = 0x0000000000000008L;
+ public const ulong ALLFILE_MSK = 0x000000000000000FL;
+ public const ulong OPTIONS_MSK = 0x000000000000FF00L;
+ public const ulong GROUP_MSK = 0x00000000FFFF0000L;
+ public const ulong LEVEL_MSK = 0x000000FF00000000L;
+
+ public const byte OPTIONS_MSK_OFFSET = 0x08;
+ public const byte GROUP_MSK_OFFSET = 0x10;
+ public const byte LEVEL_MSK_OFFSET = 0x18;
+
+ 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
+ /// </summary>
+ public static readonly Regex SpecialCharacters = new(@"[\r\n\t\a\b\e\f#?!@$%^&*\+\-\~`|<>\{}]", RegexOptions.Compiled);
+
+ #region Password/User helper extensions
+
+ /// <summary>
+ /// Generates and sets a random password for the specified user account
+ /// </summary>
+ /// <param name="manager">The configured <see cref="IUserManager"/> to process the password update on</param>
+ /// <param name="user">The user instance to update the password on</param>
+ /// <param name="passHashing">The <see cref="PasswordHashing"/> instance to hash the random password with</param>
+ /// <param name="size">Size (in bytes) of the generated random password</param>
+ /// <returns>A value indicating the results of the event (number of rows affected, should evaluate to true)</returns>
+ /// <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)
+ {
+ _ = manager ?? throw new ArgumentNullException(nameof(manager));
+ _ = user ?? throw new ArgumentNullException(nameof(user));
+ _ = passHashing ?? throw new ArgumentNullException(nameof(passHashing));
+ if (user.IsReleased)
+ {
+ throw new ObjectDisposedException("The specifed user object has been released");
+ }
+ //Alloc a buffer
+ using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(size);
+ //Use the CGN to get a random set
+ RandomHash.GetRandomBytes(buffer.Span);
+ //Hash the new random password
+ using PrivateString passHash = passHashing.Hash(buffer.Span);
+ //Write the password to the user account
+ return await manager.UpdatePassAsync(user, passHash);
+ }
+
+
+ /// <summary>
+ /// Checks to see if the current user account was created
+ /// using a local account.
+ /// </summary>
+ /// <param name="user"></param>
+ /// <returns>True if the account is a local account, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsLocalAccount(this IUser user) => LOCAL_ACCOUNT_ORIGIN.Equals(user.GetAccountOrigin(), StringComparison.Ordinal);
+
+ /// <summary>
+ /// If this account was created by any means other than a local account creation.
+ /// Implementors can use this method to determine the origin of the account.
+ /// This field is not required
+ /// </summary>
+ /// <returns>The origin of the account</returns>
+ public static string GetAccountOrigin(this IUser ud) => ud[ACC_ORIGIN_ENTRY];
+ /// <summary>
+ /// If this account was created by any means other than a local account creation.
+ /// Implementors can use this method to specify the origin of the account. This field is not required
+ /// </summary>
+ /// <param name="ud"></param>
+ /// <param name="origin">Value of the account origin</param>
+ public static void SetAccountOrigin(this IUser ud, string origin) => ud[ACC_ORIGIN_ENTRY] = origin;
+
+ /// <summary>
+ /// Gets a random user-id generated from crypograhic random number
+ /// then hashed (SHA1) and returns a hexadecimal string
+ /// </summary>
+ /// <returns>The random string user-id</returns>
+ [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
+ /// </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)
+ {
+ return GenerateAuthorization(ev, loginMessage.ClientPublicKey, loginMessage.ClientID, user);
+ }
+
+ /// <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)
+ {
+ throw new InvalidOperationException("The session is not set or the session is not a web-based session type");
+ }
+ //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;
+ //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
+ {
+ 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];
+
+
+
+ public int ClientEncBytesWritten;
+ public readonly Span<byte> ClientEncOutputBuffer => Buffer[(64 + 1024)..];
+ public readonly ReadOnlySpan<byte> EncryptedOutput => ClientEncOutputBuffer[..ClientEncBytesWritten];
+ }
+
+ /// <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
+ /// </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)
+ {
+ //Temporary work buffer
+ using IMemoryHandle<byte> buffer = Memory.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);
+ }
+
+ /// <summary>
+ /// Determines if the client sent a token header, and it maches against the current session
+ /// </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)
+ {
+ //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 = Memory.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)
+ return CryptographicOperations.FixedTimeEquals(headerBuffer[..headerTokenLen], sessionBuffer[..sessionTokenLen]);
+ }
+ return false;
+ }
+
+ /// <summary>
+ /// Regenerates the user's login token with the public key stored
+ /// during initial logon
+ /// </summary>
+ /// <returns>The base64 of the newly encrypted secret</returns>
+ public static string? RegenerateClientToken(this HttpEntity ev)
+ {
+ 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;
+ //return the clients encrypted secret
+ return base64ClientData;
+ }
+
+ /// <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)
+ {
+ if (!session.IsSet)
+ {
+ return false;
+ }
+ //try to get the public key from the client
+ string base64PubKey = session.GetBrowserPubKey();
+ return TryEncryptClientData(base64PubKey, data, in outputBuffer);
+ }
+ /// <summary>
+ /// Tries to encrypt the specified data using the specified public key
+ /// </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)
+ {
+ if (base64PubKey.IsEmpty)
+ {
+ return false;
+ }
+ //Alloc a buffer for decoding the public key
+ using UnsafeMemoryHandle<byte> pubKeyBuffer = Memory.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;
+ }
+ /// <summary>
+ /// Tries to encrypt the specified data using the specified public key
+ /// </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>
+ /// <exception cref="CryptographicException"></exception>
+ public static ERRNO TryEncryptClientData(ReadOnlySpan<byte> rawPubKey, ReadOnlySpan<byte> data, in Span<byte> outputBuffer)
+ {
+ if (rawPubKey.IsEmpty)
+ {
+ return false;
+ }
+ //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;
+
+ /// <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];
+
+ /// <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>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void SetLogin(this HttpEntity ev, bool? localAccount = null)
+ {
+ //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);
+ }
+
+ /// <summary>
+ /// Invalidates the login status of the current connection and session (if session is loaded)
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void InvalidateLogin(this HttpEntity ev)
+ {
+ //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();
+ }
+ }
+
+ /// <summary>
+ /// Determines if the current session login cookie matches the value stored in the current session (if the session is loaded)
+ /// </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 = Memory.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) < DateTimeOffset.UtcNow)
+ {
+ //Regen login token
+ ev.SetLogin();
+ ev.Session.RegenID();
+ }
+
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /// <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
+ /// </summary>
+ /// <param name="ev"></param>
+ public static void ReconcileCookies(this HttpEntity ev)
+ {
+ //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))
+ {
+ //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);
+ }
+ }
+ }
+
+
+ /// <summary>
+ /// Stores the browser's id during a login process
+ /// </summary>
+ /// <param name="session"></param>
+ /// <param name="browserId">Browser id value to store</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void SetBrowserID(in SessionInfo session, string browserId) => session[BROWSER_ID_ENTRY] = browserId;
+
+ /// <summary>
+ /// Gets the current browser's id if it was specified during login process
+ /// </summary>
+ /// <returns>The browser's id if set, <see cref="string.Empty"/> otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string GetBrowserID(this in SessionInfo session) => session[BROWSER_ID_ENTRY];
+
+ /// <summary>
+ /// Specifies that the current session belongs to a local user-account
+ /// </summary>
+ /// <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;
+ /// <summary>
+ /// Gets a value indicating if the session belongs to a local user account
+ /// </summary>
+ /// <param name="session"></param>
+ /// <returns>True if the current user's account is a local account</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool HasLocalAccount(this in SessionInfo session) => int.TryParse(session[LOCAL_ACCOUNT_ENTRY], out int value) && value > 0;
+
+ #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 = Memory.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 = Memory.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
+ /// </summary>
+ /// <param name="session"></param>
+ /// <param name="level">64bit privilage level to compare</param>
+ /// <returns>true if the current user has at least the specified level or higher</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool HasLevel(this in SessionInfo session, byte level) => (session.Privilages & LEVEL_MSK) >= (((ulong)level << LEVEL_MSK_OFFSET) & LEVEL_MSK);
+ /// <summary>
+ /// Determines if the group ID of the current user matches the specified group
+ /// </summary>
+ /// <param name="session"></param>
+ /// <param name="groupId">Group ID to compare</param>
+ /// <returns>true if the user belongs to the group, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool HasGroup(this in SessionInfo session, ushort groupId) => (session.Privilages & GROUP_MSK) == (((ulong)groupId << GROUP_MSK_OFFSET) & GROUP_MSK);
+ /// <summary>
+ /// Determines if the current user has an equivalent option code
+ /// </summary>
+ /// <param name="session"></param>
+ /// <param name="option">Option code check</param>
+ /// <returns>true if the user options field equals the option</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool HasOption(this in SessionInfo session, byte option) => (session.Privilages & OPTIONS_MSK) == (((ulong)option << OPTIONS_MSK_OFFSET) & OPTIONS_MSK);
+
+ /// <summary>
+ /// Returns the status of the user's privlage read bit
+ /// </summary>
+ /// <returns>true if the current user has the read permission, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool CanRead(this in SessionInfo session) => (session.Privilages & READ_MSK) == READ_MSK;
+ /// <summary>
+ /// Returns the status of the user's privlage write bit
+ /// </summary>
+ /// <returns>true if the current user has the write permission, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool CanWrite(this in SessionInfo session) => (session.Privilages & WRITE_MSK) == WRITE_MSK;
+ /// <summary>
+ /// Returns the status of the user's privlage delete bit
+ /// </summary>
+ /// <returns>true if the current user has the delete permission, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool CanDelete(this in SessionInfo session) => (session.Privilages & DELETE_MSK) == DELETE_MSK;
+ #endregion
+
+ #region flc
+
+ /// <summary>
+ /// Gets the current number of failed login attempts
+ /// </summary>
+ /// <param name="user"></param>
+ /// <returns>The current number of failed login attempts</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static TimestampedCounter FailedLoginCount(this IUser user)
+ {
+ ulong value = user.GetValueType<string, ulong>(FAILED_LOGIN_ENTRY);
+ return (TimestampedCounter)value;
+ }
+ /// <summary>
+ /// Sets the number of failed login attempts for the current session
+ /// </summary>
+ /// <param name="user"></param>
+ /// <param name="value">The value to set the failed login attempt count</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void FailedLoginCount(this IUser user, uint value)
+ {
+ TimestampedCounter counter = new(value);
+ //Cast the counter to a ulong and store as a ulong
+ user.SetValueType(FAILED_LOGIN_ENTRY, (ulong)counter);
+ }
+ /// <summary>
+ /// Sets the number of failed login attempts for the current session
+ /// </summary>
+ /// <param name="user"></param>
+ /// <param name="value">The value to set the failed login attempt count</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void FailedLoginCount(this IUser user, TimestampedCounter value)
+ {
+ //Cast the counter to a ulong and store as a ulong
+ user.SetValueType(FAILED_LOGIN_ENTRY, (ulong)value);
+ }
+ /// <summary>
+ /// Increments the failed login attempt count
+ /// </summary>
+ /// <param name="user"></param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void FailedLoginIncrement(this IUser user)
+ {
+ TimestampedCounter current = user.FailedLoginCount();
+ user.FailedLoginCount(current.Count + 1);
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Accounts/INonce.cs b/Plugins.Essentials/src/Accounts/INonce.cs
new file mode 100644
index 0000000..7d53183
--- /dev/null
+++ b/Plugins.Essentials/src/Accounts/INonce.cs
@@ -0,0 +1,90 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: INonce.cs
+*
+* INonce.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 object that performs storage and computation of nonce values
+ /// </summary>
+ public interface INonce
+ {
+ /// <summary>
+ /// Generates a random nonce for the current instance and
+ /// returns a base32 encoded string.
+ /// </summary>
+ /// <param name="buffer">The buffer to write a copy of the nonce value to</param>
+ void ComputeNonce(Span<byte> buffer);
+ /// <summary>
+ /// Compares the raw nonce bytes to the current nonce to determine
+ /// if the supplied nonce value is valid
+ /// </summary>
+ /// <param name="nonceBytes">The binary value of the nonce</param>
+ /// <returns>True if the nonce values are equal, flase otherwise</returns>
+ bool VerifyNonce(ReadOnlySpan<byte> nonceBytes);
+ }
+
+ /// <summary>
+ /// Provides INonce extensions for computing/verifying nonce values
+ /// </summary>
+ public static class NonceExtensions
+ {
+ /// <summary>
+ /// Computes a base32 nonce of the specified size and returns a string
+ /// representation
+ /// </summary>
+ /// <param name="nonce"></param>
+ /// <param name="size">The size (in bytes) of the nonce</param>
+ /// <returns>The base32 string of the computed nonce</returns>
+ public static string ComputeNonce<T>(this T nonce, int size) where T: INonce
+ {
+ //Alloc bin buffer
+ using UnsafeMemoryHandle<byte> buffer = Memory.UnsafeAlloc<byte>(size);
+ //Compute nonce
+ nonce.ComputeNonce(buffer.Span);
+ //Return base32 string
+ return VnEncoding.ToBase32String(buffer.Span, false);
+ }
+ /// <summary>
+ /// Compares the base32 encoded nonce value against the previously
+ /// generated nonce
+ /// </summary>
+ /// <param name="nonce"></param>
+ /// <param name="base32Nonce">The base32 encoded nonce string</param>
+ /// <returns>True if the nonce values are equal, flase otherwise</returns>
+ public static bool VerifyNonce<T>(this T nonce, ReadOnlySpan<char> base32Nonce) where T : INonce
+ {
+ //Alloc bin buffer
+ using UnsafeMemoryHandle<byte> buffer = Memory.UnsafeAlloc<byte>(base32Nonce.Length);
+ //Decode base32 nonce
+ ERRNO count = VnEncoding.TryFromBase32Chars(base32Nonce, buffer.Span);
+ //Verify nonce
+ return nonce.VerifyNonce(buffer.Span[..(int)count]);
+ }
+ }
+}
diff --git a/Plugins.Essentials/src/Accounts/LoginMessage.cs b/Plugins.Essentials/src/Accounts/LoginMessage.cs
new file mode 100644
index 0000000..ebc616e
--- /dev/null
+++ b/Plugins.Essentials/src/Accounts/LoginMessage.cs
@@ -0,0 +1,102 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: LoginMessage.cs
+*
+* LoginMessage.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 System.Text.Json.Serialization;
+using VNLib.Utils.Memory;
+
+namespace VNLib.Plugins.Essentials.Accounts
+{
+ /// <summary>
+ /// A uniform JSON login message for the
+ /// accounts provider to use
+ /// </summary>
+ /// <remarks>
+ /// NOTE: This class derrives from <see cref="PrivateStringManager"/>
+ /// and should be disposed properly
+ /// </remarks>
+ public class LoginMessage : PrivateStringManager
+ {
+ /// <summary>
+ /// A property
+ /// </summary>
+ [JsonPropertyName("username")]
+ public string UserName { get; set; }
+ /// <summary>
+ /// A protected string property that
+ /// may represent a user's password
+ /// </summary>
+ [JsonPropertyName("password")]
+ public string Password
+ {
+ get => base[0];
+ set => base[0] = value;
+ }
+ [JsonPropertyName("localtime")]
+ public string Lt
+ {
+ get => LocalTime.ToString("O");
+ //Try to parse the supplied time string, and use the datetime.min if the time string is invalid
+ set => LocalTime = DateTimeOffset.TryParse(value, out DateTimeOffset local) ? local : DateTimeOffset.MinValue;
+ }
+
+ /// <summary>
+ /// Represents the clients local time in a <see cref="DateTime"/> struct
+ /// </summary>
+ [JsonIgnore]
+ public DateTimeOffset LocalTime { get; set; }
+ /// <summary>
+ /// The clients specified local-language
+ /// </summary>
+ [JsonPropertyName("locallanguage")]
+ public string LocalLanguage { get; set; }
+ /// <summary>
+ /// The clients shared public key used for encryption, this property is not protected
+ /// </summary>
+ [JsonPropertyName("pubkey")]
+ public string ClientPublicKey { get; set; }
+ /// <summary>
+ /// The clients browser id if shared
+ /// </summary>
+ [JsonPropertyName("clientid")]
+ public string ClientID { get; set; }
+ /// <summary>
+ /// Initailzies a new <see cref="LoginMessage"/> and its parent <see cref="PrivateStringManager"/>
+ /// base
+ /// </summary>
+ public LoginMessage() : this(1) { }
+ /// <summary>
+ /// Allows for derrives classes to have multple protected
+ /// string elements
+ /// </summary>
+ /// <param name="protectedElementSize">
+ /// The number of procted string elements required
+ /// </param>
+ /// <remarks>
+ /// NOTE: <paramref name="protectedElementSize"/> must be at-least 1
+ /// or access to <see cref="Password"/> will throw
+ /// </remarks>
+ protected LoginMessage(int protectedElementSize = 1) : base(protectedElementSize) { }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Accounts/PasswordHashing.cs b/Plugins.Essentials/src/Accounts/PasswordHashing.cs
new file mode 100644
index 0000000..1c3770b
--- /dev/null
+++ b/Plugins.Essentials/src/Accounts/PasswordHashing.cs
@@ -0,0 +1,244 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: PasswordHashing.cs
+*
+* PasswordHashing.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 System.Security.Cryptography;
+
+using VNLib.Hashing;
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+
+namespace VNLib.Plugins.Essentials.Accounts
+{
+ /// <summary>
+ /// A delegate method to recover a temporary copy of the secret/pepper
+ /// for a request
+ /// </summary>
+ /// <param name="buffer">The buffer to write the pepper to</param>
+ /// <returns>The number of bytes written to the buffer</returns>
+ public delegate ERRNO SecretAction(Span<byte> buffer);
+
+ /// <summary>
+ /// Provides a structrued password hashing system implementing the <seealso cref="VnArgon2"/> library
+ /// with fixed time comparison
+ /// </summary>
+ public sealed class PasswordHashing
+ {
+ private readonly SecretAction _getter;
+ private readonly int _secretSize;
+
+ private readonly uint TimeCost;
+ private readonly uint MemoryCost;
+ private readonly uint HashLen;
+ private readonly int SaltLen;
+ private readonly uint Parallelism;
+
+ /// <summary>
+ /// Initalizes the <see cref="PasswordHashing"/> class
+ /// </summary>
+ /// <param name="getter"></param>
+ /// <param name="secreteSize">The expected size of the secret (the size of the buffer to alloc for a copy)</param>
+ /// <param name="saltLen">A positive integer for the size of the random salt used during the hashing proccess</param>
+ /// <param name="timeCost">The Argon2 time cost parameter</param>
+ /// <param name="memoryCost">The Argon2 memory cost parameter</param>
+ /// <param name="hashLen">The size of the hash to produce during hashing operations</param>
+ /// <param name="parallism">
+ /// The Argon2 parallelism parameter (the number of threads to use for hasing)
+ /// (default = 0 - the number of processors)
+ /// </param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public PasswordHashing(SecretAction getter, int secreteSize, int saltLen = 32, uint timeCost = 4, uint memoryCost = UInt16.MaxValue, uint parallism = 0, uint hashLen = 128)
+ {
+ //Store getter
+ _getter = getter ?? throw new ArgumentNullException(nameof(getter));
+ _secretSize = secreteSize;
+
+ //Store parameters
+ HashLen = hashLen;
+ //Store maginitude as a unit
+ MemoryCost = memoryCost;
+ TimeCost = timeCost;
+ 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>
+ public bool Verify(ReadOnlySpan<char> passHash, ReadOnlySpan<char> password)
+ {
+ if(passHash.IsEmpty || password.IsEmpty)
+ {
+ return false;
+ }
+ //alloc secret buffer
+ using UnsafeMemoryHandle<byte> secretBuffer = Memory.UnsafeAlloc<byte>(_secretSize, true);
+ try
+ {
+ //Get the secret from the callback
+ ERRNO count = _getter(secretBuffer.Span);
+ //Verify
+ return VnArgon2.Verify2id(password, passHash, secretBuffer.Span[..(int)count]);
+ }
+ finally
+ {
+ //Erase secret buffer
+ Memory.InitializeBlock(secretBuffer.Span);
+ }
+ }
+ /// <summary>
+ /// Verifies a password against its hash. Partially exposes the Argon2 api.
+ /// </summary>
+ /// <param name="hash">Previously hashed password</param>
+ /// <param name="salt">The salt used to hash the original password</param>
+ /// <param name="password">The password to hash and compare against </param>
+ /// <returns>true if bytes derrived from password match the hash, false otherwise</returns>
+ /// <exception cref="VnArgon2Exception"></exception>
+ /// <remarks>Uses fixed time comparison from <see cref="CryptographicOperations"/> class</remarks>
+ public bool Verify(ReadOnlySpan<byte> hash, ReadOnlySpan<byte> salt, ReadOnlySpan<byte> password)
+ {
+ //Alloc a buffer with the same size as the hash
+ using UnsafeMemoryHandle<byte> hashBuf = Memory.UnsafeAlloc<byte>(hash.Length, true);
+ //Hash the password with the current config
+ Hash(password, salt, hashBuf.Span);
+ //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>
+ /// <exception cref="VnArgon2Exception"></exception>
+ /// <returns>A <see cref="PrivateString"/> of the hashed and encoded password</returns>
+ public PrivateString Hash(ReadOnlySpan<char> password)
+ {
+ //Alloc shared buffer for the salt and secret buffer
+ using UnsafeMemoryHandle<byte> buffer = Memory.UnsafeAlloc<byte>(SaltLen + _secretSize, true);
+ try
+ {
+ //Split buffers
+ Span<byte> saltBuf = buffer.Span[..SaltLen];
+ Span<byte> secretBuf = buffer.Span[SaltLen..];
+
+ //Fill the buffer with random bytes
+ RandomHash.GetRandomBytes(saltBuf);
+
+ //recover the secret
+ ERRNO count = _getter(secretBuf);
+
+ //Hashes a password, with the current parameters
+ return (PrivateString)VnArgon2.Hash2id(password, saltBuf, secretBuf[..(int)count], TimeCost, MemoryCost, Parallelism, HashLen);
+ }
+ finally
+ {
+ Memory.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>
+ /// <exception cref="VnArgon2Exception"></exception>
+ /// <returns>A <see cref="PrivateString"/> of the hashed and encoded password</returns>
+ public PrivateString Hash(ReadOnlySpan<byte> password)
+ {
+ using UnsafeMemoryHandle<byte> buffer = Memory.UnsafeAlloc<byte>(SaltLen + _secretSize, true);
+ try
+ {
+ //Split buffers
+ Span<byte> saltBuf = buffer.Span[..SaltLen];
+ Span<byte> secretBuf = buffer.Span[SaltLen..];
+
+ //Fill the buffer with random bytes
+ RandomHash.GetRandomBytes(saltBuf);
+
+ //recover the secret
+ ERRNO count = _getter(secretBuf);
+
+ //Hashes a password, with the current parameters
+ return (PrivateString)VnArgon2.Hash2id(password, saltBuf, secretBuf[..(int)count], TimeCost, MemoryCost, Parallelism, HashLen);
+ }
+ finally
+ {
+ Memory.InitializeBlock(buffer.Span);
+ }
+ }
+ /// <summary>
+ /// Partially exposes the Argon2 api. Hashes the specified password, with the initialized pepper.
+ /// Writes the raw hash output to the specified buffer
+ /// </summary>
+ /// <param name="password">Password to be hashed</param>
+ /// <param name="salt">Salt to hash the password with</param>
+ /// <param name="hashOutput">The output buffer to store the hashed password to. The exact length of this buffer is the hash size</param>
+ /// <exception cref="VnArgon2Exception"></exception>
+ public void Hash(ReadOnlySpan<byte> password, ReadOnlySpan<byte> salt, Span<byte> hashOutput)
+ {
+ //alloc secret buffer
+ using UnsafeMemoryHandle<byte> secretBuffer = Memory.UnsafeAlloc<byte>(_secretSize, true);
+ try
+ {
+ //Get the secret from the callback
+ ERRNO count = _getter(secretBuffer.Span);
+ //Hashes a password, with the current parameters
+ VnArgon2.Hash2id(password, salt, secretBuffer.Span[..(int)count], hashOutput, TimeCost, MemoryCost, Parallelism);
+ }
+ finally
+ {
+ //Erase secret buffer
+ Memory.InitializeBlock(secretBuffer.Span);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Content/IPageRouter.cs b/Plugins.Essentials/src/Content/IPageRouter.cs
new file mode 100644
index 0000000..e6952f4
--- /dev/null
+++ b/Plugins.Essentials/src/Content/IPageRouter.cs
@@ -0,0 +1,43 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: IPageRouter.cs
+*
+* IPageRouter.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.Threading.Tasks;
+
+//Import account system for privilage masks
+
+namespace VNLib.Plugins.Essentials.Content
+{
+ /// <summary>
+ /// Determines file routines (routing) for incomming connections
+ /// </summary>
+ public interface IPageRouter
+ {
+ /// <summary>
+ /// Determines what file path to return to a user for the given incoming connection
+ /// </summary>
+ /// <param name="entity">The connection to proccess</param>
+ /// <returns>A <see cref="ValueTask"/> that returns the <see cref="FileProcessArgs"/> to pass to the file processor</returns>
+ ValueTask<FileProcessArgs> RouteAsync(HttpEntity entity);
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs b/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs
new file mode 100644
index 0000000..bced960
--- /dev/null
+++ b/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs
@@ -0,0 +1,58 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: ProtectedWebEndpoint.cs
+*
+* ProtectedWebEndpoint.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 System.Net;
+
+using VNLib.Utils;
+using VNLib.Plugins.Essentials.Accounts;
+
+namespace VNLib.Plugins.Essentials.Endpoints
+{
+ /// <summary>
+ /// Implements <see cref="UnprotectedWebEndpoint"/> to provide
+ /// authoriation checks before processing
+ /// </summary>
+ public abstract class ProtectedWebEndpoint : UnprotectedWebEndpoint
+ {
+ ///<inheritdoc/>
+ protected override ERRNO PreProccess(HttpEntity entity)
+ {
+ if (!base.PreProccess(entity))
+ {
+ return false;
+ }
+ //The loggged in flag must be set, and the token must also match
+ if (!entity.LoginCookieMatches() || !entity.TokenMatches())
+ {
+ //Return unauthorized status
+ entity.CloseResponse(HttpStatusCode.Unauthorized);
+ //A return value less than 0 signals a virtual skip event
+ return -1;
+ }
+ //Continue
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Endpoints/ProtectionSettings.cs b/Plugins.Essentials/src/Endpoints/ProtectionSettings.cs
new file mode 100644
index 0000000..77620ac
--- /dev/null
+++ b/Plugins.Essentials/src/Endpoints/ProtectionSettings.cs
@@ -0,0 +1,103 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: ProtectionSettings.cs
+*
+* ProtectionSettings.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;
+
+namespace VNLib.Plugins.Essentials.Endpoints
+{
+ /// <summary>
+ /// A data structure containing a basic security protocol
+ /// for connection pre-checks. Settings are the most
+ /// strict by default
+ /// </summary>
+ public readonly struct ProtectionSettings : IEquatable<ProtectionSettings>
+ {
+ /// <summary>
+ /// Requires TLS be enabled for all incomming requets (or loopback adapter)
+ /// </summary>
+ public readonly bool DisabledTlsRequired { get; init; }
+
+ /// <summary>
+ /// Checks that sessions are enabled for incomming requests
+ /// and that they are not new sessions.
+ /// </summary>
+ public readonly bool DisableSessionsRequired { get; init; }
+
+ /// <summary>
+ /// Allows connections that define cross-site sec headers
+ /// to be processed or denied (denied by default)
+ /// </summary>
+ public readonly bool DisableCrossSiteDenied { get; init; }
+
+ /// <summary>
+ /// Enables referr match protection. Requires that if a referer header is
+ /// set that it matches the current origin
+ /// </summary>
+ public readonly bool DisableRefererMatch { get; init; }
+
+ /// <summary>
+ /// Requires all connections to have pass an IsBrowser() check
+ /// (requires a valid user-agent header that contains Mozilla in
+ /// the string)
+ /// </summary>
+ public readonly bool DisableBrowsersOnly { get; init; }
+
+ /// <summary>
+ /// If the connection has a valid session, verifies that the
+ /// stored session origin matches the client's origin header.
+ /// (confirms the session is coming from the same origin it
+ /// was created on)
+ /// </summary>
+ public readonly bool DisableVerifySessionCors { get; init; }
+
+ /// <summary>
+ /// Disables response caching, by setting the cache control headers appropriatly.
+ /// Default is disabled
+ /// </summary>
+ public readonly bool EnableCaching { get; init; }
+
+
+ ///<inheritdoc/>
+ public override bool Equals(object obj) => obj is ProtectionSettings settings && Equals(settings);
+ ///<inheritdoc/>
+ public override int GetHashCode() => base.GetHashCode();
+
+ ///<inheritdoc/>
+ public static bool operator ==(ProtectionSettings left, ProtectionSettings right) => left.Equals(right);
+ ///<inheritdoc/>
+ public static bool operator !=(ProtectionSettings left, ProtectionSettings right) => !(left == right);
+
+ ///<inheritdoc/>
+ public bool Equals(ProtectionSettings other)
+ {
+ return DisabledTlsRequired == other.DisabledTlsRequired &&
+ DisableSessionsRequired == other.DisableSessionsRequired &&
+ DisableCrossSiteDenied == other.DisableCrossSiteDenied &&
+ DisableRefererMatch == other.DisableRefererMatch &&
+ DisableBrowsersOnly == other.DisableBrowsersOnly &&
+ DisableVerifySessionCors == other.DisableVerifySessionCors &&
+ EnableCaching == other.EnableCaching;
+ }
+ }
+}
diff --git a/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs b/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs
new file mode 100644
index 0000000..4af3c30
--- /dev/null
+++ b/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs
@@ -0,0 +1,346 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: ResourceEndpointBase.cs
+*
+* ResourceEndpointBase.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 System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+
+using VNLib.Net.Http;
+using VNLib.Utils;
+using VNLib.Utils.Logging;
+using VNLib.Plugins.Essentials.Extensions;
+
+namespace VNLib.Plugins.Essentials.Endpoints
+{
+
+ /// <summary>
+ /// Provides a base class for implementing un-authenticated resource endpoints
+ /// with basic (configurable) security checks
+ /// </summary>
+ public abstract class ResourceEndpointBase : VirtualEndpoint<HttpEntity>
+ {
+ /// <summary>
+ /// Default protection settings. Protection settings are the most
+ /// secure by default, should be loosened an necessary
+ /// </summary>
+ protected virtual ProtectionSettings EndpointProtectionSettings { get; }
+
+ ///<inheritdoc/>
+ public override async ValueTask<VfReturnType> Process(HttpEntity entity)
+ {
+ try
+ {
+ ERRNO preProc = PreProccess(entity);
+ if (preProc == ERRNO.E_FAIL)
+ {
+ return VfReturnType.Forbidden;
+ }
+ //Entity was responded to by the pre-processor
+ if (preProc < 0)
+ {
+ return VfReturnType.VirtualSkip;
+ }
+ //If websockets are quested allow them to be processed in a logged-in/secure context
+ if (entity.Server.IsWebSocketRequest)
+ {
+ return await WebsocketRequestedAsync(entity);
+ }
+ ValueTask<VfReturnType> op = entity.Server.Method switch
+ {
+ //Get request to get account
+ HttpMethod.GET => GetAsync(entity),
+ HttpMethod.POST => PostAsync(entity),
+ HttpMethod.DELETE => DeleteAsync(entity),
+ HttpMethod.PUT => PutAsync(entity),
+ HttpMethod.PATCH => PatchAsync(entity),
+ HttpMethod.OPTIONS => OptionsAsync(entity),
+ _ => AlternateMethodAsync(entity, entity.Server.Method),
+ };
+ return await op;
+ }
+ catch (InvalidJsonRequestException ije)
+ {
+ //Write the je to debug log
+ Log.Debug(ije, "Failed to de-serialize a request entity body");
+ //If the method is not POST/PUT/PATCH return a json message
+ if ((entity.Server.Method & (HttpMethod.HEAD | HttpMethod.OPTIONS | HttpMethod.TRACE | HttpMethod.DELETE)) > 0)
+ {
+ return VfReturnType.BadRequest;
+ }
+ //Only allow if json is an accepted response type
+ if (!entity.Server.Accepts(ContentType.Json))
+ {
+ return VfReturnType.BadRequest;
+ }
+ //Build web-message
+ WebMessage webm = new()
+ {
+ Result = "Request body is not valid json"
+ };
+ //Set the response webm
+ entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
+ //return virtual
+ return VfReturnType.VirtualSkip;
+ }
+ catch (TerminateConnectionException)
+ {
+ //A TC exception is intentional and should always propagate to the runtime
+ throw;
+ }
+ catch (ContentTypeUnacceptableException)
+ {
+ /*
+ * The runtime will handle a 406 unaccetptable response
+ * and invoke the proper error app handler
+ */
+ throw;
+ }
+ //Re-throw exceptions that are cause by reading the transport layer
+ catch (IOException ioe) when (ioe.InnerException is SocketException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ //Log an uncaught excetpion and return an error code (log may not be initialized)
+ Log?.Error(ex);
+ return VfReturnType.Error;
+ }
+ }
+
+ /// <summary>
+ /// Allows for synchronous Pre-Processing of an entity. The result
+ /// will determine if the method processing methods will be invoked, or
+ /// a <see cref="VfReturnType.Forbidden"/> error code will be returned
+ /// </summary>
+ /// <param name="entity">The incomming request to process</param>
+ /// <returns>
+ /// True if processing should continue, false if the response should be
+ /// <see cref="VfReturnType.Forbidden"/>, less than 0 if entity was
+ /// responded to.
+ /// </returns>
+ protected virtual ERRNO PreProccess(HttpEntity entity)
+ {
+ //Disable cache if requested
+ if (!EndpointProtectionSettings.EnableCaching)
+ {
+ entity.Server.SetNoCache();
+ }
+
+ //Enforce TLS
+ if (!EndpointProtectionSettings.DisabledTlsRequired && !entity.IsSecure && !entity.IsLocalConnection)
+ {
+ return false;
+ }
+
+ //Enforce browser check
+ if (!EndpointProtectionSettings.DisableBrowsersOnly && !entity.Server.IsBrowser())
+ {
+ return false;
+ }
+
+ //Enforce refer check
+ if (!EndpointProtectionSettings.DisableRefererMatch && entity.Server.Referer != null && !entity.Server.RefererMatch())
+ {
+ return false;
+ }
+
+ //enforce session basic
+ if (!EndpointProtectionSettings.DisableSessionsRequired && (!entity.Session.IsSet || entity.Session.IsNew))
+ {
+ return false;
+ }
+
+ /*
+ * If sessions are required, verify cors is set, and the client supplied an origin header,
+ * verify that it matches the origin that was specified during session initialization
+ */
+ if ((!EndpointProtectionSettings.DisableSessionsRequired & !EndpointProtectionSettings.DisableVerifySessionCors) && entity.Server.Origin != null && !entity.Session.CrossOriginMatch)
+ {
+ return false;
+ }
+
+ //Enforce cross-site
+ if (!EndpointProtectionSettings.DisableCrossSiteDenied && entity.Server.IsCrossSite())
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// This method gets invoked when an incoming POST request to the endpoint has been requested.
+ /// </summary>
+ /// <param name="entity">The entity to be processed</param>
+ /// <returns>The result of the operation to return to the file processor</returns>
+ protected virtual ValueTask<VfReturnType> PostAsync(HttpEntity entity)
+ {
+ return ValueTask.FromResult(Post(entity));
+ }
+ /// <summary>
+ /// This method gets invoked when an incoming GET request to the endpoint has been requested.
+ /// </summary>
+ /// <param name="entity">The entity to be processed</param>
+ /// <returns>The result of the operation to return to the file processor</returns>
+ protected virtual ValueTask<VfReturnType> GetAsync(HttpEntity entity)
+ {
+ return ValueTask.FromResult(Get(entity));
+ }
+ /// <summary>
+ /// This method gets invoked when an incoming DELETE request to the endpoint has been requested.
+ /// </summary>
+ /// <param name="entity">The entity to be processed</param>
+ /// <returns>The result of the operation to return to the file processor</returns>
+ protected virtual ValueTask<VfReturnType> DeleteAsync(HttpEntity entity)
+ {
+ return ValueTask.FromResult(Delete(entity));
+ }
+ /// <summary>
+ /// This method gets invoked when an incoming PUT request to the endpoint has been requested.
+ /// </summary>
+ /// <param name="entity">The entity to be processed</param>
+ /// <returns>The result of the operation to return to the file processor</returns>
+ protected virtual ValueTask<VfReturnType> PutAsync(HttpEntity entity)
+ {
+ return ValueTask.FromResult(Put(entity));
+ }
+ /// <summary>
+ /// This method gets invoked when an incoming PATCH request to the endpoint has been requested.
+ /// </summary>
+ /// <param name="entity">The entity to be processed</param>
+ /// <returns>The result of the operation to return to the file processor</returns>
+ protected virtual ValueTask<VfReturnType> PatchAsync(HttpEntity entity)
+ {
+ return ValueTask.FromResult(Patch(entity));
+ }
+
+ protected virtual ValueTask<VfReturnType> OptionsAsync(HttpEntity entity)
+ {
+ return ValueTask.FromResult(Options(entity));
+ }
+
+ /// <summary>
+ /// Invoked when a request is received for a method other than GET, POST, DELETE, or PUT;
+ /// </summary>
+ /// <param name="entity">The entity that </param>
+ /// <param name="method">The request method</param>
+ /// <returns>The results of the processing</returns>
+ protected virtual ValueTask<VfReturnType> AlternateMethodAsync(HttpEntity entity, HttpMethod method)
+ {
+ return ValueTask.FromResult(AlternateMethod(entity, method));
+ }
+
+ /// <summary>
+ /// Invoked when the current endpoint received a websocket request
+ /// </summary>
+ /// <param name="entity">The entity that requested the websocket</param>
+ /// <returns>The results of the operation</returns>
+ protected virtual ValueTask<VfReturnType> WebsocketRequestedAsync(HttpEntity entity)
+ {
+ return ValueTask.FromResult(WebsocketRequested(entity));
+ }
+
+ /// <summary>
+ /// This method gets invoked when an incoming POST request to the endpoint has been requested.
+ /// </summary>
+ /// <param name="entity">The entity to be processed</param>
+ /// <returns>The result of the operation to return to the file processor</returns>
+ protected virtual VfReturnType Post(HttpEntity entity)
+ {
+ //Return method not allowed
+ entity.CloseResponse(HttpStatusCode.MethodNotAllowed);
+ return VfReturnType.VirtualSkip;
+ }
+ /// <summary>
+ /// This method gets invoked when an incoming GET request to the endpoint has been requested.
+ /// </summary>
+ /// <param name="entity">The entity to be processed</param>
+ /// <returns>The result of the operation to return to the file processor</returns>
+ protected virtual VfReturnType Get(HttpEntity entity)
+ {
+ return VfReturnType.ProcessAsFile;
+ }
+ /// <summary>
+ /// This method gets invoked when an incoming DELETE request to the endpoint has been requested.
+ /// </summary>
+ /// <param name="entity">The entity to be processed</param>
+ /// <returns>The result of the operation to return to the file processor</returns>
+ protected virtual VfReturnType Delete(HttpEntity entity)
+ {
+ entity.CloseResponse(HttpStatusCode.MethodNotAllowed);
+ return VfReturnType.VirtualSkip;
+ }
+ /// <summary>
+ /// This method gets invoked when an incoming PUT request to the endpoint has been requested.
+ /// </summary>
+ /// <param name="entity">The entity to be processed</param>
+ /// <returns>The result of the operation to return to the file processor</returns>
+ protected virtual VfReturnType Put(HttpEntity entity)
+ {
+ entity.CloseResponse(HttpStatusCode.MethodNotAllowed);
+ return VfReturnType.VirtualSkip;
+ }
+ /// <summary>
+ /// This method gets invoked when an incoming PATCH request to the endpoint has been requested.
+ /// </summary>
+ /// <param name="entity">The entity to be processed</param>
+ /// <returns>The result of the operation to return to the file processor</returns>
+ protected virtual VfReturnType Patch(HttpEntity entity)
+ {
+ entity.CloseResponse(HttpStatusCode.MethodNotAllowed);
+ return VfReturnType.VirtualSkip;
+ }
+ /// <summary>
+ /// Invoked when a request is received for a method other than GET, POST, DELETE, or PUT;
+ /// </summary>
+ /// <param name="entity">The entity that </param>
+ /// <param name="method">The request method</param>
+ /// <returns>The results of the processing</returns>
+ protected virtual VfReturnType AlternateMethod(HttpEntity entity, HttpMethod method)
+ {
+ //Return method not allowed
+ entity.CloseResponse(HttpStatusCode.MethodNotAllowed);
+ return VfReturnType.VirtualSkip;
+ }
+
+ protected virtual VfReturnType Options(HttpEntity entity)
+ {
+ return VfReturnType.Forbidden;
+ }
+
+ /// <summary>
+ /// Invoked when the current endpoint received a websocket request
+ /// </summary>
+ /// <param name="entity">The entity that requested the websocket</param>
+ /// <returns>The results of the operation</returns>
+ protected virtual VfReturnType WebsocketRequested(HttpEntity entity)
+ {
+ entity.CloseResponse(HttpStatusCode.Forbidden);
+ return VfReturnType.VirtualSkip;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Endpoints/UnprotectedWebEndpoint.cs b/Plugins.Essentials/src/Endpoints/UnprotectedWebEndpoint.cs
new file mode 100644
index 0000000..cc923c7
--- /dev/null
+++ b/Plugins.Essentials/src/Endpoints/UnprotectedWebEndpoint.cs
@@ -0,0 +1,42 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: UnprotectedWebEndpoint.cs
+*
+* UnprotectedWebEndpoint.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 VNLib.Utils;
+
+namespace VNLib.Plugins.Essentials.Endpoints
+{
+ /// <summary>
+ /// A base class for un-authenticated web (browser) based resource endpoints
+ /// to implement. Adds additional security checks
+ /// </summary>
+ public abstract class UnprotectedWebEndpoint : ResourceEndpointBase
+ {
+ ///<inheritdoc/>
+ protected override ERRNO PreProccess(HttpEntity entity)
+ {
+ return base.PreProccess(entity)
+ && (!entity.Session.IsSet || entity.Session.SessionType == Sessions.SessionType.Web);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Endpoints/VirtualEndpoint.cs b/Plugins.Essentials/src/Endpoints/VirtualEndpoint.cs
new file mode 100644
index 0000000..5beb4b9
--- /dev/null
+++ b/Plugins.Essentials/src/Endpoints/VirtualEndpoint.cs
@@ -0,0 +1,67 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: VirtualEndpoint.cs
+*
+* VirtualEndpoint.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 System.Threading.Tasks;
+
+using VNLib.Utils.Logging;
+
+namespace VNLib.Plugins.Essentials.Endpoints
+{
+ /// <summary>
+ /// Provides a base class for <see cref="IVirtualEndpoint{T}"/> entity processors
+ /// with checks and a log provider
+ /// </summary>
+ /// <typeparam name="T">The entity type to process</typeparam>
+ public abstract class VirtualEndpoint<T> : MarshalByRefObject, IVirtualEndpoint<T>
+ {
+ ///<inheritdoc/>
+ public virtual string Path { get; protected set; }
+
+ /// <summary>
+ /// An <see cref="ILogProvider"/> to write logs to
+ /// </summary>
+ protected ILogProvider Log { get; private set; }
+
+ /// <summary>
+ /// Sets the log and path and checks the values
+ /// </summary>
+ /// <param name="Path">The path this instance represents</param>
+ /// <param name="log">The log provider that will be used</param>
+ /// <exception cref="ArgumentException"></exception>
+ protected void InitPathAndLog(string Path, ILogProvider log)
+ {
+ if (string.IsNullOrWhiteSpace(Path) || Path[0] != '/')
+ {
+ throw new ArgumentException("Path must begin with a '/' character", nameof(Path));
+ }
+ //Store path
+ this.Path = Path;
+ //Store log
+ Log = log ?? throw new ArgumentNullException(nameof(log));
+ }
+ ///<inheritdoc/>
+ public abstract ValueTask<VfReturnType> Process(T entity);
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/EventProcessor.cs b/Plugins.Essentials/src/EventProcessor.cs
new file mode 100644
index 0000000..826ed00
--- /dev/null
+++ b/Plugins.Essentials/src/EventProcessor.cs
@@ -0,0 +1,728 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: EventProcessor.cs
+*
+* EventProcessor.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 System.IO;
+using System.Net;
+using System.Linq;
+using System.Threading;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using VNLib.Net;
+using VNLib.Net.Http;
+using VNLib.Utils.IO;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Resources;
+using VNLib.Plugins.Essentials.Content;
+using VNLib.Plugins.Essentials.Sessions;
+using VNLib.Plugins.Essentials.Extensions;
+
+#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
+ {
+ private static readonly AsyncLocal<EventProcessor?> _currentProcessor = new();
+
+ /// <summary>
+ /// Gets the current (ambient) async local event processor
+ /// </summary>
+ public static EventProcessor? Current => _currentProcessor.Value;
+
+ /// <summary>
+ /// The filesystem entrypoint path for the site
+ /// </summary>
+ public abstract string Directory { get; }
+ ///<inheritdoc/>
+ public abstract string Hostname { get; }
+
+ /// <summary>
+ /// Gets the EP processing options
+ /// </summary>
+ public abstract IEpProcessingOptions Options { get; }
+
+ /// <summary>
+ /// Event log provider
+ /// </summary>
+ protected abstract ILogProvider Log { 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>
+ public abstract string TranslateResourcePath(string requestPath);
+ /// <summary>
+ /// <para>
+ /// When an error occurs and is handled by the library, this event is invoked
+ /// </para>
+ /// <para>
+ /// NOTE: This function must be thread-safe!
+ /// </para>
+ /// </summary>
+ /// <param name="errorCode">The error code that was created during processing</param>
+ /// <param name="entity">The active IHttpEvent representing the faulted request</param>
+ /// <returns>A value indicating if the entity was proccsed by this call</returns>
+ public abstract bool ErrorHandler(HttpStatusCode errorCode, IHttpEvent entity);
+ /// <summary>
+ /// For pre-processing a request entity before all endpoint lookups are performed
+ /// </summary>
+ /// <param name="entity">The http entity to process</param>
+ /// <returns>The results to return to the file processor, or null of the entity requires further processing</returns>
+ public abstract ValueTask<FileProcessArgs> PreProcessEntityAsync(HttpEntity entity);
+ /// <summary>
+ /// Allows for post processing of a selected <see cref="FileProcessArgs"/> for the given entity
+ /// </summary>
+ /// <param name="entity">The http entity to process</param>
+ /// <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();
+
+ /// <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);
+ }
+
+ #endregion
+
+ #region sessions
+
+ /// <summary>
+ /// An <see cref="ISessionProvider"/> that connects stateful sessions to
+ /// HTTP connections
+ /// </summary>
+ private ISessionProvider? Sessions;
+
+ /// <summary>
+ /// Sets or resets the current <see cref="ISessionProvider"/>
+ /// for all connections
+ /// </summary>
+ /// <param name="sp">The new <see cref="ISessionProvider"/></param>
+ public void SetSessionProvider(ISessionProvider? sp) => _ = Interlocked.Exchange(ref Sessions, sp);
+
+ #endregion
+
+ #region router
+
+ /// <summary>
+ /// An <see cref="IPageRouter"/> to route files to be processed
+ /// </summary>
+ private IPageRouter? Router;
+
+ /// <summary>
+ /// Sets or resets the current <see cref="IPageRouter"/>
+ /// for all connections
+ /// </summary>
+ /// <param name="router"><see cref="IPageRouter"/> to route incomming connections</param>
+ public void SetPageRouter(IPageRouter? router) => _ = Interlocked.Exchange(ref Router, router);
+
+ #endregion
+
+ #region Virtual Endpoints
+
+ /*
+ * Wrapper class for converting IHttpEvent endpoints to
+ * httpEntityEndpoints
+ */
+ private class EvEndpointWrapper : IVirtualEndpoint<HttpEntity>
+ {
+ private readonly IVirtualEndpoint<IHttpEvent> _wrapped;
+ public EvEndpointWrapper(IVirtualEndpoint<IHttpEvent> wrapping) => _wrapped = wrapping;
+ string IEndpoint.Path => _wrapped.Path;
+ ValueTask<VfReturnType> IVirtualEndpoint<HttpEntity>.Process(HttpEntity entity) => _wrapped.Process(entity);
+ }
+
+
+ /*
+ * The VE table is read-only for the processor and my only
+ * be updated by the application via the methods below
+ *
+ * Since it would be very inefficient to track endpoint users
+ * using locks, we can assume any endpoint that is currently
+ * processing requests cannot be stopped, so we just focus on
+ * swapping the table when updates need to be made.
+ *
+ * This means calls to modify the table will read the table
+ * (clone it), modify the local copy, then exhange it for
+ * the active table so new requests will be processed on the
+ * new table.
+ *
+ * To make the calls to modify the table thread safe, a lock is
+ * held while modification operations run, then the updated
+ * copy is published. Any threads reading the old table
+ * will continue to use a stale endpoint.
+ */
+
+ /// <summary>
+ /// 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();
+
+
+ /*
+ * A lock that is held by callers that intend to
+ * modify the vep table at the same time
+ */
+ private readonly object VeUpdateLock = new();
+
+
+ /// <summary>
+ /// Determines the endpoint type(s) and adds them to the endpoint store(s) as necessary
+ /// </summary>
+ /// <param name="endpoints">Params array of endpoints to add to the store</param>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ public void AddEndpoint(params IEndpoint[] endpoints)
+ {
+ //Check
+ _ = endpoints ?? throw new ArgumentNullException(nameof(endpoints));
+ //Make sure all endpoints specify a path
+ if(endpoints.Any(static e => string.IsNullOrWhiteSpace(e?.Path)))
+ {
+ throw new ArgumentException("Endpoints array contains one or more empty endpoints");
+ }
+
+ if (endpoints.Length == 0)
+ {
+ return;
+ }
+
+ //Get virtual endpoints
+ IEnumerable<IVirtualEndpoint<HttpEntity>> eps = endpoints
+ .Where(static e => e is IVirtualEndpoint<HttpEntity>)
+ .Select(static e => (IVirtualEndpoint<HttpEntity>)e);
+
+ //Get http event endpoints and create wrapper classes for conversion
+ IEnumerable<IVirtualEndpoint<HttpEntity>> evs = endpoints
+ .Where(static e => e is IVirtualEndpoint<IHttpEvent>)
+ .Select(static e => new EvEndpointWrapper((e as IVirtualEndpoint<IHttpEvent>)!));
+
+ //Uinion endpoints by their paths to combine them
+ IEnumerable<IVirtualEndpoint<HttpEntity>> allEndpoints = eps.UnionBy(evs, static s => s.Path);
+
+ lock (VeUpdateLock)
+ {
+ //Clone the current dictonary
+ Dictionary<string, IVirtualEndpoint<HttpEntity>> newTable = new(VirtualEndpoints, StringComparer.OrdinalIgnoreCase);
+ //Insert the new eps, and/or overwrite old eps
+ foreach(IVirtualEndpoint<HttpEntity> ep in allEndpoints)
+ {
+ newTable.Add(ep.Path, ep);
+ }
+
+ //Store the new table
+ _ = Interlocked.Exchange(ref VirtualEndpoints, newTable);
+ }
+ }
+
+ /// <summary>
+ /// Removes the specified endpoint from the virtual store and oauthendpoints if eneabled and found
+ /// </summary>
+ /// <param name="eps">A collection of endpoints to remove from the table</param>
+ public void RemoveEndpoint(params IEndpoint[] eps)
+ {
+ _ = eps ?? throw new ArgumentNullException(nameof(eps));
+ //Call remove on path
+ RemoveVirtualEndpoint(eps.Select(static s => s.Path).ToArray());
+ }
+
+ /// <summary>
+ /// Stops listening for connections to the specified <see cref="IVirtualEndpoint{T}"/> identified by its path
+ /// </summary>
+ /// <param name="paths">An array of endpoint paths to remove from the table</param>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ public void RemoveVirtualEndpoint(params string[] paths)
+ {
+ _ = paths ?? throw new ArgumentNullException(nameof(paths));
+ //Make sure all endpoints specify a path
+ if (paths.Any(static e => string.IsNullOrWhiteSpace(e)))
+ {
+ throw new ArgumentException("Paths array contains one or more empty strings");
+ }
+
+ if(paths.Length == 0)
+ {
+ return;
+ }
+
+ //take update lock
+ lock (VeUpdateLock)
+ {
+ //Clone the current dictonary
+ Dictionary<string, IVirtualEndpoint<HttpEntity>> newTable = new(VirtualEndpoints, StringComparer.OrdinalIgnoreCase);
+
+ foreach(string eps in paths)
+ {
+ _ = newTable.Remove(eps);
+ }
+ //Store the new table ony if the endpoint existed
+ _ = Interlocked.Exchange(ref VirtualEndpoints, newTable);
+ }
+ }
+
+ #endregion
+
+ ///<inheritdoc/>
+ public virtual async ValueTask ClientConnectedAsync(IHttpEvent httpEvent)
+ {
+ //load ref to session provider
+ ISessionProvider? _sessions = Sessions;
+
+ //Set ambient processor context
+ _currentProcessor.Value = this;
+ //Start cancellation token
+ CancellationTokenSource timeout = new(Options.ExecutionTimeout);
+ try
+ {
+ //Session handle, default to the shared empty session
+ SessionHandle sessionHandle = default;
+
+ //If sessions are set, get a session for the current connection
+ if (_sessions != null)
+ {
+ //Get the session
+ sessionHandle = await _sessions.GetSessionAsync(httpEvent, timeout.Token);
+ //If the processor had an error recovering the session, return the result to the processor
+ if (sessionHandle.EntityStatus != FileProcessArgs.Continue)
+ {
+ ProcessFile(httpEvent, sessionHandle.EntityStatus);
+ return;
+ }
+ }
+ try
+ {
+ //Setup entity
+ HttpEntity entity = new(httpEvent, this, in sessionHandle, timeout.Token);
+ //Pre-process entity
+ FileProcessArgs preProc = await PreProcessEntityAsync(entity);
+ //If preprocess returned a value, exit
+ if (preProc != FileProcessArgs.Continue)
+ {
+ ProcessFile(httpEvent, in preProc);
+ return;
+ }
+
+ if (VirtualEndpoints.Count > 0)
+ {
+ //Process a virtual file
+ FileProcessArgs virtualArgs = await ProcessVirtualAsync(entity);
+ //If the entity was processed, exit
+ if (virtualArgs != FileProcessArgs.Continue)
+ {
+ ProcessFile(httpEvent, in virtualArgs);
+ return;
+ }
+ }
+ //If no virtual processor handled the ws request, deny it
+ if (entity.Server.IsWebSocketRequest)
+ {
+ ProcessFile(httpEvent, in FileProcessArgs.Deny);
+ return;
+ }
+ //Finally process as file
+ FileProcessArgs args = await RouteFileAsync(entity);
+ //Finally process the file
+ ProcessFile(httpEvent, in args);
+ }
+ finally
+ {
+ //Capture all session release exceptions
+ try
+ {
+ //Release the session
+ await sessionHandle.ReleaseAsync(httpEvent);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Exception raised while releasing the assocated session");
+ }
+ }
+ }
+ catch (ContentTypeUnacceptableException)
+ {
+ /*
+ * The user application attempted to set a content that the client does not accept
+ * Assuming this exception was uncaught by application code, there should not be
+ * any response body, either way we should respond with the unacceptable status code
+ */
+ CloseWithError(HttpStatusCode.NotAcceptable, httpEvent);
+ }
+ catch (TerminateConnectionException)
+ {
+ throw;
+ }
+ catch (ResourceUpdateFailedException ruf)
+ {
+ Log.Warn(ruf);
+ CloseWithError(HttpStatusCode.ServiceUnavailable, httpEvent);
+ }
+ catch (SessionException se)
+ {
+ Log.Warn(se, "An exception was raised while attempting to get or save a session");
+ CloseWithError(HttpStatusCode.ServiceUnavailable, httpEvent);
+ return;
+ }
+ catch (OperationCanceledException oce)
+ {
+ Log.Warn(oce, "Request execution time exceeded, connection terminated");
+ CloseWithError(HttpStatusCode.ServiceUnavailable, httpEvent);
+ }
+ catch (IOException ioe) when (ioe.InnerException is SocketException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Log.Warn(ex, "Unhandled exception during application code execution.");
+ //Invoke the root error handler
+ CloseWithError(HttpStatusCode.InternalServerError, httpEvent);
+ }
+ finally
+ {
+ timeout.Dispose();
+ _currentProcessor.Value = null;
+ }
+ }
+
+ /// <summary>
+ /// Accepts the entity to process a file for an the selected <see cref="FileProcessArgs"/>
+ /// by user code and determines what file-system file to open and respond to the connection with.
+ /// </summary>
+ /// <param name="entity">The entity to process the file for</param>
+ /// <param name="args">The selected <see cref="FileProcessArgs"/> to determine what file to process</param>
+ protected virtual void ProcessFile(IHttpEvent entity, in FileProcessArgs args)
+ {
+ try
+ {
+ string? filename = null;
+ //Switch on routine
+ switch (args.Routine)
+ {
+ //Close the connection with an error state
+ case FpRoutine.Error:
+ CloseWithError(HttpStatusCode.InternalServerError, entity);
+ return;
+ //Redirect the user
+ case FpRoutine.Redirect:
+ //Set status code
+ entity.Redirect(RedirectType.Found, args.Alternate);
+ return;
+ //Deny
+ case FpRoutine.Deny:
+ CloseWithError(HttpStatusCode.Forbidden, entity);
+ return;
+ //Not return not found
+ case FpRoutine.NotFound:
+ CloseWithError(HttpStatusCode.NotFound, entity);
+ return;
+ //Serve other file
+ case FpRoutine.ServeOther:
+ {
+ //Use the specified relative alternate path the user specified
+ if (FindResourceInRoot(args.Alternate, out string otherPath))
+ {
+ filename = otherPath;
+ }
+ }
+ break;
+ //Normal file lookup
+ case FpRoutine.Continue:
+ {
+ //Lookup the file based on the client requested local path
+ if (FindResourceInRoot(entity.Server.Path, out string path))
+ {
+ filename = path;
+ }
+ }
+ break;
+ //The user indicated that the file is a fully qualified path, and should be treated directly
+ case FpRoutine.ServeOtherFQ:
+ {
+ //Get the absolute path of the file rooted in the current server root and determine if it exists
+ if (FindResourceInRoot(args.Alternate, true, out string fqPath))
+ {
+ filename = fqPath;
+ }
+ }
+ break;
+ //The user has indicated they handled all necessary action, and we will exit
+ case FpRoutine.VirtualSkip:
+ return;
+ default:
+ break;
+ }
+ //If the file was not set or the request method is not a GET (or HEAD), return not-found
+ if (filename == null || (entity.Server.Method & (HttpMethod.GET | HttpMethod.HEAD)) == 0)
+ {
+ CloseWithError(HttpStatusCode.NotFound, entity);
+ return;
+ }
+ DateTime fileLastModified = File.GetLastWriteTimeUtc(filename);
+ //See if the last modifed header was set
+ DateTimeOffset? ifModifedSince = entity.Server.LastModified();
+ //If the header was set, check the date, if the file has been modified since, continue sending the file
+ if (ifModifedSince != null && ifModifedSince.Value > fileLastModified)
+ {
+ //File has not been modified
+ entity.CloseResponse(HttpStatusCode.NotModified);
+ return;
+ }
+ //Get the content type of he file
+ ContentType fileType = HttpHelpers.GetContentTypeFromFile(filename);
+ //Make sure the client accepts the content type
+ if (entity.Server.Accepts(fileType))
+ {
+ //set last modified time as the files last write time
+ entity.Server.LastModified(fileLastModified);
+ //try to open the selected file for reading and allow sharing
+ FileStream fs = new (filename, FileMode.Open, FileAccess.Read, FileShare.Read);
+ //Check for range
+ if (entity.Server.Range != null && entity.Server.Range.Item1 > 0)
+ {
+ //Seek the stream to the specified position
+ fs.Position = entity.Server.Range.Item1;
+ entity.CloseResponse(HttpStatusCode.PartialContent, fileType, fs);
+ }
+ else
+ {
+ //send the whole file
+ entity.CloseResponse(HttpStatusCode.OK, fileType, fs);
+ }
+ return;
+ }
+ else
+ {
+ //Unacceptable
+ CloseWithError(HttpStatusCode.NotAcceptable, entity);
+ return;
+ }
+ }
+ catch (IOException ioe)
+ {
+ Log.Information(ioe, "Unhandled exception during file opening.");
+ CloseWithError(HttpStatusCode.Locked, entity);
+ return;
+ }
+ catch (Exception ex)
+ {
+ Log.Information(ex, "Unhandled exception during file opening.");
+ //Invoke the root error handler
+ CloseWithError(HttpStatusCode.InternalServerError, entity);
+ return;
+ }
+ }
+
+ private void CloseWithError(HttpStatusCode code, IHttpEvent entity)
+ {
+ //Invoke the inherited error handler
+ if (!ErrorHandler(code, entity))
+ {
+ //Disable cache
+ entity.Server.SetNoCache();
+ //Error handler does not have a response for the error code, so return a generic error code
+ entity.CloseResponse(code);
+ }
+ }
+
+
+ /// <summary>
+ /// If virtual endpoints are enabled, checks for the existance of an
+ /// endpoint and attmepts to process that endpoint.
+ /// </summary>
+ /// <param name="entity">The http entity to proccess</param>
+ /// <returns>The results to return to the file processor, or null of the entity requires further processing</returns>
+ protected virtual async ValueTask<FileProcessArgs> ProcessVirtualAsync(HttpEntity entity)
+ {
+ //See if the virtual file is servicable
+ if (!VirtualEndpoints.TryGetValue(entity.Server.Path, out IVirtualEndpoint<HttpEntity>? vf))
+ {
+ return FileProcessArgs.Continue;
+ }
+
+ //Invoke the page handler process method
+ VfReturnType rt = await vf.Process(entity);
+
+ if (rt == VfReturnType.VirtualSkip)
+ {
+ //Virtual file was handled by the handler
+ return FileProcessArgs.VirtualSkip;
+ }
+ else if(rt == VfReturnType.ProcessAsFile)
+ {
+ return FileProcessArgs.Continue;
+ }
+
+ //If not a get request, process it directly
+ if (entity.Server.Method == HttpMethod.GET)
+ {
+ switch (rt)
+ {
+ case VfReturnType.Forbidden:
+ return FileProcessArgs.Deny;
+ case VfReturnType.NotFound:
+ return FileProcessArgs.NotFound;
+ case VfReturnType.Error:
+ return FileProcessArgs.Error;
+ default:
+ break;
+ }
+ }
+
+ switch (rt)
+ {
+ case VfReturnType.Forbidden:
+ entity.CloseResponse(HttpStatusCode.Forbidden);
+ break;
+ case VfReturnType.BadRequest:
+ entity.CloseResponse(HttpStatusCode.BadRequest);
+ break;
+ case VfReturnType.Error:
+ entity.CloseResponse(HttpStatusCode.InternalServerError);
+ break;
+ case VfReturnType.NotFound:
+ default:
+ entity.CloseResponse(HttpStatusCode.NotFound);
+ break;
+ }
+
+ return FileProcessArgs.VirtualSkip;
+ }
+
+ /// <summary>
+ /// Determines the best <see cref="FileProcessArgs"/> processing response for the given connection.
+ /// Alternativley may respond to the entity directly.
+ /// </summary>
+ /// <param name="entity">The http entity to process</param>
+ /// <returns>The results to return to the file processor, this method must return an argument</returns>
+ protected virtual async ValueTask<FileProcessArgs> RouteFileAsync(HttpEntity entity)
+ {
+ //Read local copy of the router
+
+ IPageRouter? router = Router;
+ //Make sure router is set
+ if (router == null)
+ {
+ return FileProcessArgs.Continue;
+ }
+ //Get a file routine
+ FileProcessArgs routine = await router.RouteAsync(entity);
+ //Call post processor method
+ PostProcessFile(entity, in routine);
+ //Return the routine
+ return routine;
+ }
+
+
+ /// <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>
+ public bool FindResourceInRoot(string resourcePath, bool fullyQualified, out string path)
+ {
+ //Special case where user's can specify a fullly qualified path (meant to reach a remote file, eg UNC/network share or other disk)
+ if (fullyQualified && Path.IsPathRooted(resourcePath) && Path.IsPathFullyQualified(resourcePath) && FileOperations.FileExists(resourcePath))
+ {
+ path = resourcePath;
+ return true;
+ }
+ //Otherwise invoke non fully qualified path
+ return FindResourceInRoot(resourcePath, out 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>
+ public bool FindResourceInRoot(string resourcePath, out string path)
+ {
+ //Check after fully qualified path name because above is a special case
+ path = TranslateResourcePath(resourcePath);
+ string extension = Path.GetExtension(path);
+ //Make sure extension isnt blocked
+ if (Options.ExcludedExtensions.Contains(extension))
+ {
+ return false;
+ }
+ //Trailing / means dir, so look for a default file (index.html etc) (most likely so check first?)
+ if (Path.EndsInDirectorySeparator(path))
+ {
+ string comp = path;
+ //Find default file if blank
+ foreach (string d in Options.DefaultFiles)
+ {
+ path = Path.Combine(comp, d);
+ if (FileOperations.FileExists(path))
+ {
+ //Get attributes
+ FileAttributes att = FileOperations.GetAttributes(path);
+ //Make sure the file is accessable and isnt an unsafe file
+ return ((att & Options.AllowedAttributes) > 0) && ((att & Options.DissallowedAttributes) == 0);
+ }
+ }
+ }
+ //try the file as is
+ else if (FileOperations.FileExists(path))
+ {
+ //Get attributes
+ FileAttributes att = FileOperations.GetAttributes(path);
+ //Make sure the file is accessable and isnt an unsafe file
+ return ((att & Options.AllowedAttributes) > 0) && ((att & Options.DissallowedAttributes) == 0);
+ }
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/CollectionsExtensions.cs b/Plugins.Essentials/src/Extensions/CollectionsExtensions.cs
new file mode 100644
index 0000000..9500d5e
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/CollectionsExtensions.cs
@@ -0,0 +1,85 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: CollectionsExtensions.cs
+*
+* CollectionsExtensions.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 System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ /// <summary>
+ ///
+ /// </summary>
+ public static class CollectionsExtensions
+ {
+ /// <summary>
+ /// Gets a value by the specified key if it exsits and the value is not null/empty
+ /// </summary>
+ /// <param name="dict"></param>
+ /// <param name="key">Key associated with the value</param>
+ /// <param name="value">Value associated with the key</param>
+ /// <returns>True of the key is found and is not noll/empty, false otherwise</returns>
+ public static bool TryGetNonEmptyValue(this IReadOnlyDictionary<string, string> dict, string key, [MaybeNullWhen(false)] out string value)
+ {
+ if (dict.TryGetValue(key, out string? val) && !string.IsNullOrWhiteSpace(val))
+ {
+ value = val;
+ return true;
+ }
+ value = null;
+ return false;
+ }
+ /// <summary>
+ /// Determines if an argument was set in a <see cref="IReadOnlyDictionary{TKey, TValue}"/> by comparing
+ /// the value stored at the key, to the type argument
+ /// </summary>
+ /// <param name="dict"></param>
+ /// <param name="key">The argument's key</param>
+ /// <param name="argument">The argument to compare against</param>
+ /// <returns>
+ /// True if the key was found, and the value at the key is equal to the type parameter. False if the key is null/empty, or the
+ /// value does not match the specified type
+ /// </returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static bool IsArgumentSet(this IReadOnlyDictionary<string, string> dict, string key, ReadOnlySpan<char> argument)
+ {
+ //Try to get the value from the dict, if the value is null casting it to span (implicitly) should stop null excpetions and return false
+ return dict.TryGetValue(key, out string? value) && string.GetHashCode(argument) == string.GetHashCode(value);
+ }
+ /// <summary>
+ ///
+ /// </summary>
+ /// <typeparam name="TKey"></typeparam>
+ /// <typeparam name="TValue"></typeparam>
+ /// <param name="dict"></param>
+ /// <param name="key"></param>
+ /// <returns></returns>
+ public static TValue? GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key)
+ {
+ return dict.TryGetValue(key, out TValue? value) ? value : default;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs b/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
new file mode 100644
index 0000000..ba01132
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
@@ -0,0 +1,361 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: ConnectionInfoExtensions.cs
+*
+* ConnectionInfoExtensions.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 System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Runtime.CompilerServices;
+
+using VNLib.Net.Http;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ /// <summary>
+ /// Provides <see cref="ConnectionInfo"/> extension methods
+ /// for common use cases
+ /// </summary>
+ public static class IConnectionInfoExtensions
+ {
+ public const string SEC_HEADER_MODE = "Sec-Fetch-Mode";
+ public const string SEC_HEADER_SITE = "Sec-Fetch-Site";
+ public const string SEC_HEADER_USER = "Sec-Fetch-User";
+ public const string SEC_HEADER_DEST = "Sec-Fetch-Dest";
+ public const string X_FORWARDED_FOR_HEADER = "x-forwarded-for";
+ public const string X_FORWARDED_PROTO_HEADER = "x-forwarded-proto";
+ public const string DNT_HEADER = "dnt";
+
+ /// <summary>
+ /// Cache-Control header value for disabling cache
+ /// </summary>
+ public static readonly string NO_CACHE_RESPONSE_HEADER_VALUE = HttpHelpers.GetCacheString(CacheType.NoCache | CacheType.NoStore | CacheType.Revalidate);
+
+ /// <summary>
+ /// Gets the <see cref="HttpRequestHeader.IfModifiedSince"/> header value and converts its value to a datetime value
+ /// </summary>
+ /// <returns>The if modified-since header date-time, null if the header was not set or the value was invalid</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static DateTimeOffset? LastModified(this IConnectionInfo server)
+ {
+ //Get the if-modified-since header
+ string? ifModifiedSince = server.Headers[HttpRequestHeader.IfModifiedSince];
+ //Make sure tis set and try to convert it to a date-time structure
+ return DateTimeOffset.TryParse(ifModifiedSince, out DateTimeOffset d) ? d : null;
+ }
+
+ /// <summary>
+ /// Sets the last-modified response header value
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="value">Time the entity was last modified</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void LastModified(this IConnectionInfo server, DateTimeOffset value)
+ {
+ server.Headers[HttpResponseHeader.LastModified] = value.ToString("R");
+ }
+
+ /// <summary>
+ /// Is the connection requesting cors
+ /// </summary>
+ /// <returns>true if the user-agent specified the cors security header</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsCors(this IConnectionInfo server) => "cors".Equals(server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
+ /// <summary>
+ /// Determines if the User-Agent specified "cross-site" in the Sec-Site header, OR
+ /// the connection spcified an origin header and the origin's host does not match the
+ /// requested host
+ /// </summary>
+ /// <returns>true if the request originated from a site other than the current one</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsCrossSite(this IConnectionInfo server)
+ {
+ return "cross-site".Equals(server.Headers[SEC_HEADER_SITE], StringComparison.OrdinalIgnoreCase)
+ || (server.Origin != null && !server.RequestUri.DnsSafeHost.Equals(server.Origin.DnsSafeHost, StringComparison.Ordinal));
+ }
+ /// <summary>
+ /// Is the connection user-agent created, or automatic
+ /// </summary>
+ /// <param name="server"></param>
+ /// <returns>true if sec-user header was set to "?1"</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsUserInvoked(this IConnectionInfo server) => "?1".Equals(server.Headers[SEC_HEADER_USER], StringComparison.OrdinalIgnoreCase);
+ /// <summary>
+ /// Was this request created from normal user navigation
+ /// </summary>
+ /// <returns>true if sec-mode set to "navigate"</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsNavigation(this IConnectionInfo server) => "navigate".Equals(server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
+ /// <summary>
+ /// Determines if the client specified "no-cache" for the cache control header, signalling they do not wish to cache the entity
+ /// </summary>
+ /// <returns>True if <see cref="HttpRequestHeader.CacheControl"/> contains the string "no-cache", false otherwise</returns>
+ public static bool NoCache(this IConnectionInfo server)
+ {
+ string? cache_header = server.Headers[HttpRequestHeader.CacheControl];
+ return !string.IsNullOrWhiteSpace(cache_header) && cache_header.Contains("no-cache", StringComparison.OrdinalIgnoreCase);
+ }
+ /// <summary>
+ /// Sets the response cache headers to match the requested caching type. Does not check against request headers
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="type">One or more <see cref="CacheType"/> flags that identify the way the entity can be cached</param>
+ /// <param name="maxAge">The max age the entity is valid for</param>
+ public static void SetCache(this IConnectionInfo server, CacheType type, TimeSpan maxAge)
+ {
+ //If no cache flag is set, set the pragma header to no-cache
+ if((type & CacheType.NoCache) > 0)
+ {
+ server.Headers[HttpResponseHeader.Pragma] = "no-cache";
+ }
+ //Set the cache hader string using the http helper class
+ server.Headers[HttpResponseHeader.CacheControl] = HttpHelpers.GetCacheString(type, maxAge);
+ }
+ /// <summary>
+ /// Sets the Cache-Control response header to <see cref="NO_CACHE_RESPONSE_HEADER_VALUE"/>
+ /// and the pragma response header to 'no-cache'
+ /// </summary>
+ /// <param name="server"></param>
+ public static void SetNoCache(this IConnectionInfo server)
+ {
+ //Set default nocache string
+ server.Headers[HttpResponseHeader.CacheControl] = NO_CACHE_RESPONSE_HEADER_VALUE;
+ server.Headers[HttpResponseHeader.Pragma] = "no-cache";
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the port number in the request is equivalent to the port number
+ /// on the local server.
+ /// </summary>
+ /// <returns>True if the port number in the <see cref="ConnectionInfo.RequestUri"/> matches the
+ /// <see cref="ConnectionInfo.LocalEndpoint"/> port false if they do not match
+ /// </returns>
+ /// <remarks>
+ /// Users should call this method to help prevent port based attacks if your
+ /// code relies on the port number of the <see cref="ConnectionInfo.RequestUri"/>
+ /// </remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool EnpointPortsMatch(this IConnectionInfo server)
+ {
+ return server.RequestUri.Port == server.LocalEndpoint.Port;
+ }
+ /// <summary>
+ /// Determines if the host of the current request URI matches the referer header host
+ /// </summary>
+ /// <returns>True if the request host and the referer host paremeters match, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool RefererMatch(this IConnectionInfo server)
+ {
+ return server.RequestUri.DnsSafeHost.Equals(server.Referer?.DnsSafeHost, StringComparison.OrdinalIgnoreCase);
+ }
+ /// <summary>
+ /// Expires a client's cookie
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="name"></param>
+ /// <param name="domain"></param>
+ /// <param name="path"></param>
+ /// <param name="sameSite"></param>
+ /// <param name="secure"></param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ExpireCookie(this IConnectionInfo server, string name, string domain = "", string path = "/", CookieSameSite sameSite = CookieSameSite.None, bool secure = false)
+ {
+ server.SetCookie(name, string.Empty, domain, path, TimeSpan.Zero, sameSite, false, secure);
+ }
+ /// <summary>
+ /// Sets a cookie with an infinite (session life-span)
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="name"></param>
+ /// <param name="value"></param>
+ /// <param name="domain"></param>
+ /// <param name="path"></param>
+ /// <param name="sameSite"></param>
+ /// <param name="httpOnly"></param>
+ /// <param name="secure"></param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void SetSessionCookie(
+ this IConnectionInfo server,
+ string name,
+ string value,
+ string domain = "",
+ string path = "/",
+ CookieSameSite sameSite = CookieSameSite.None,
+ bool httpOnly = false,
+ bool secure = false)
+ {
+ server.SetCookie(name, value, domain, path, TimeSpan.MaxValue, sameSite, httpOnly, secure);
+ }
+
+ /// <summary>
+ /// Sets a cookie with an infinite (session life-span)
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="name"></param>
+ /// <param name="value"></param>
+ /// <param name="domain"></param>
+ /// <param name="path"></param>
+ /// <param name="sameSite"></param>
+ /// <param name="httpOnly"></param>
+ /// <param name="secure"></param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void SetCookie(
+ this IConnectionInfo server,
+ string name,
+ string value,
+ TimeSpan expires,
+ string domain = "",
+ string path = "/",
+ CookieSameSite sameSite = CookieSameSite.None,
+ bool httpOnly = false,
+ bool secure = false)
+ {
+ server.SetCookie(name, value, domain, path, expires, sameSite, httpOnly, secure);
+ }
+
+ /// <summary>
+ /// Is the current connection a "browser" ?
+ /// </summary>
+ /// <param name="server"></param>
+ /// <returns>true if the user agent string contains "Mozilla" and does not contain "bot", false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsBrowser(this IConnectionInfo server)
+ {
+ //Get user-agent and determine if its a browser
+ return server.UserAgent != null && !server.UserAgent.Contains("bot", StringComparison.OrdinalIgnoreCase) && server.UserAgent.Contains("Mozilla", StringComparison.OrdinalIgnoreCase);
+ }
+ /// <summary>
+ /// Determines if the current connection is the loopback/internal network adapter
+ /// </summary>
+ /// <param name="server"></param>
+ /// <returns>True of the connection was made from the local machine</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsLoopBack(this IConnectionInfo server)
+ {
+ IPAddress realIp = server.GetTrustedIp();
+ return IPAddress.Any.Equals(realIp) || IPAddress.Loopback.Equals(realIp);
+ }
+
+ /// <summary>
+ /// Did the connection set the dnt header?
+ /// </summary>
+ /// <returns>true if the connection specified the dnt header, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool DNT(this IConnectionInfo server) => !string.IsNullOrWhiteSpace(server.Headers[DNT_HEADER]);
+
+ /// <summary>
+ /// Determins if the current connection is behind a trusted downstream server
+ /// </summary>
+ /// <param name="server"></param>
+ /// <returns>True if the connection came from a trusted downstream server, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsBehindDownStreamServer(this IConnectionInfo server)
+ {
+ //See if there is an ambient event processor
+ EventProcessor? ev = EventProcessor.Current;
+ //See if the connection is coming from an downstream server
+ return ev != null && ev.Options.DownStreamServers.Contains(server.RemoteEndpoint.Address);
+ }
+
+ /// <summary>
+ /// Gets the real IP address of the request if behind a trusted downstream server, otherwise returns the transport remote ip address
+ /// </summary>
+ /// <param name="server"></param>
+ /// <returns>The real ip of the connection</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IPAddress GetTrustedIp(this IConnectionInfo server) => GetTrustedIp(server, server.IsBehindDownStreamServer());
+ /// <summary>
+ /// Gets the real IP address of the request if behind a trusted downstream server, otherwise returns the transport remote ip address
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="isTrusted"></param>
+ /// <returns>The real ip of the connection</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static IPAddress GetTrustedIp(this IConnectionInfo server, bool isTrusted)
+ {
+ //If the connection is not trusted, then ignore header parsing
+ if (isTrusted)
+ {
+ //Nginx sets a header identifying the remote ip address so parse it
+ string? real_ip = server.Headers[X_FORWARDED_FOR_HEADER];
+ //If the real-ip header is set, try to parse is and return the address found, otherwise return the remote ep
+ return !string.IsNullOrWhiteSpace(real_ip) && IPAddress.TryParse(real_ip, out IPAddress? addr) ? addr : server.RemoteEndpoint.Address;
+ }
+ else
+ {
+ return server.RemoteEndpoint.Address;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value that determines if the connection is using tls, locally
+ /// or behind a trusted downstream server that is using tls.
+ /// </summary>
+ /// <param name="server"></param>
+ /// <returns>True if the connection is secure, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsSecure(this IConnectionInfo server)
+ {
+ //Get value of the trusted downstream server
+ return IsSecure(server, server.IsBehindDownStreamServer());
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool IsSecure(this IConnectionInfo server, bool isTrusted)
+ {
+ //If the connection is not trusted, then ignore header parsing
+ if (isTrusted)
+ {
+ //Standard https protocol header
+ string? protocol = server.Headers[X_FORWARDED_PROTO_HEADER];
+ //If the header is set and equals https then tls is being used
+ return string.IsNullOrWhiteSpace(protocol) ? server.IsSecure : "https".Equals(protocol, StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ return server.IsSecure;
+ }
+ }
+
+ /// <summary>
+ /// Was the connection made on a local network to the server? NOTE: Use with caution
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsLocalConnection(this IConnectionInfo server) => server.LocalEndpoint.Address.IsLocalSubnet(server.GetTrustedIp());
+
+ /// <summary>
+ /// Get a cookie from the current request
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="name">Name/ID of cookie</param>
+ /// <param name="cookieValue">Is set to cookie if found, or null if not</param>
+ /// <returns>True if cookie exists and was retrieved</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool GetCookie(this IConnectionInfo server, string name, [NotNullWhen(true)] out string? cookieValue)
+ {
+ //Try to get a cookie from the request
+ return server.RequestCookies.TryGetValue(name, out cookieValue);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
new file mode 100644
index 0000000..9458487
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
@@ -0,0 +1,848 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: EssentialHttpEventExtensions.cs
+*
+* EssentialHttpEventExtensions.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 System.IO;
+using System.Net;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+using VNLib.Net.Http;
+using VNLib.Hashing;
+using VNLib.Utils;
+using VNLib.Utils.Extensions;
+using VNLib.Utils.Memory.Caching;
+using static VNLib.Plugins.Essentials.Statics;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+
+ /// <summary>
+ /// Provides extension methods for manipulating <see cref="HttpEvent"/>s
+ /// </summary>
+ public static class EssentialHttpEventExtensions
+ {
+ public const string BEARER_STRING = "Bearer";
+ private static readonly int BEARER_LEN = BEARER_STRING.Length;
+
+ /*
+ * Pooled/tlocal serializers
+ */
+ private static ThreadLocal<Utf8JsonWriter> LocalSerializer { get; } = new(() => new(Stream.Null));
+ private static IObjectRental<JsonResponse> ResponsePool { get; } = ObjectRental.Create(ResponseCtor);
+ private static JsonResponse ResponseCtor() => new(ResponsePool);
+
+ #region Response Configuring
+
+ /// <summary>
+ /// Attempts to serialize the JSON object (with default SR_OPTIONS) to binary and configure the response for a JSON message body
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="ev"></param>
+ /// <param name="code">The <see cref="HttpStatusCode"/> result of the connection</param>
+ /// <param name="response">The JSON object to serialzie and send as response body</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseJson<T>(this IHttpEvent ev, HttpStatusCode code, T response) => CloseResponseJson(ev, code, response, SR_OPTIONS);
+
+ /// <summary>
+ /// Attempts to serialize the JSON object to binary and configure the response for a JSON message body
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="ev"></param>
+ /// <param name="code">The <see cref="HttpStatusCode"/> result of the connection</param>
+ /// <param name="response">The JSON object to serialzie and send as response body</param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during serialization</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseJson<T>(this IHttpEvent ev, HttpStatusCode code, T response, JsonSerializerOptions? options)
+ {
+ JsonResponse rbuf = ResponsePool.Rent();
+ try
+ {
+ //Serialze the object on the thread local serializer
+ LocalSerializer.Value!.Serialize(rbuf, response, options);
+
+ //Set the response as the buffer,
+ ev.CloseResponse(code, ContentType.Json, rbuf);
+ }
+ catch
+ {
+ //Return back to pool on error
+ ResponsePool.Return(rbuf);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Attempts to serialize the JSON object to binary and configure the response for a JSON message body
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The <see cref="HttpStatusCode"/> result of the connection</param>
+ /// <param name="response">The JSON object to serialzie and send as response body</param>
+ /// <param name="type">The type to use during de-serialization</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseJson(this IHttpEvent ev, HttpStatusCode code, object response, Type type) => CloseResponseJson(ev, code, response, type, SR_OPTIONS);
+
+ /// <summary>
+ /// Attempts to serialize the JSON object to binary and configure the response for a JSON message body
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The <see cref="HttpStatusCode"/> result of the connection</param>
+ /// <param name="response">The JSON object to serialzie and send as response body</param>
+ /// <param name="type">The type to use during de-serialization</param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during serialization</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseJson(this IHttpEvent ev, HttpStatusCode code, object response, Type type, JsonSerializerOptions? options)
+ {
+ JsonResponse rbuf = ResponsePool.Rent();
+ try
+ {
+ //Serialze the object on the thread local serializer
+ LocalSerializer.Value!.Serialize(rbuf, response, type, options);
+
+ //Set the response as the buffer,
+ ev.CloseResponse(code, ContentType.Json, rbuf);
+ }
+ catch
+ {
+ //Return back to pool on error
+ ResponsePool.Return(rbuf);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Writes the <see cref="JsonDocument"/> data to a temporary buffer and sets it as the response
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The <see cref="HttpStatusCode"/> result of the connection</param>
+ /// <param name="data">The <see cref="JsonDocument"/> data to send to client</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseJson(this IHttpEvent ev, HttpStatusCode code, JsonDocument data)
+ {
+ if(data == null)
+ {
+ ev.CloseResponse(code);
+ return;
+ }
+
+ JsonResponse rbuf = ResponsePool.Rent();
+ try
+ {
+ //Serialze the object on the thread local serializer
+ LocalSerializer.Value!.Serialize(rbuf, data);
+
+ //Set the response as the buffer,
+ ev.CloseResponse(code, ContentType.Json, rbuf);
+ }
+ catch
+ {
+ //Return back to pool on error
+ ResponsePool.Return(rbuf);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Close as response to a client with an <see cref="HttpStatusCode.OK"/> and serializes a <see cref="WebMessage"/> as the message response
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="webm">The <see cref="WebMessage"/> to serialize and response to client with</param>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponse<T>(this IHttpEvent ev, T webm) where T:WebMessage
+ {
+ if (webm == null)
+ {
+ ev.CloseResponse(HttpStatusCode.OK);
+ }
+ else
+ {
+ //Respond with json data
+ ev.CloseResponseJson(HttpStatusCode.OK, webm);
+ }
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a file as an attachment (set content dispostion)
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">Status code</param>
+ /// <param name="file">The <see cref="FileInfo"/> of the desired file to attach</param>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="FileNotFoundException"></exception>
+ /// <exception cref="UnauthorizedAccessException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ /// <exception cref="System.Security.SecurityException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, FileInfo file)
+ {
+ //Close with file
+ ev.CloseResponse(code, file);
+ //Set content dispostion as attachment (only if successfull)
+ ev.Server.Headers["Content-Disposition"] = $"attachment; filename=\"{file.Name}\"";
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a file as an attachment (set content dispostion)
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">Status code</param>
+ /// <param name="file">The <see cref="FileStream"/> of the desired file to attach</param>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, FileStream file)
+ {
+ //Close with file
+ ev.CloseResponse(code, file);
+ //Set content dispostion as attachment (only if successfull)
+ ev.Server.Headers["Content-Disposition"] = $"attachment; filename=\"{file.Name}\"";
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a file as an attachment (set content dispostion)
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">Status code</param>
+ /// <param name="data">The data to straem to the client as an attatcment</param>
+ /// <param name="ct">The <see cref="ContentType"/> that represents the file</param>
+ /// <param name="fileName">The name of the file to attach</param>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, ContentType ct, Stream data, string fileName)
+ {
+ //Close with file
+ ev.CloseResponse(code, ct, data);
+ //Set content dispostion as attachment (only if successfull)
+ ev.Server.Headers["Content-Disposition"] = $"attachment; filename=\"{fileName}\"";
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a file as the entire response body (not attachment)
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">Status code</param>
+ /// <param name="file">The <see cref="FileInfo"/> of the desired file to attach</param>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="FileNotFoundException"></exception>
+ /// <exception cref="UnauthorizedAccessException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ /// <exception cref="System.Security.SecurityException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, FileInfo file)
+ {
+ //Open filestream for file
+ FileStream fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
+ try
+ {
+ //Set the input as a stream
+ ev.CloseResponse(code, fs);
+ //Set last modified time only if successfull
+ ev.Server.Headers[HttpResponseHeader.LastModified] = file.LastWriteTimeUtc.ToString("R");
+ }
+ catch
+ {
+ //If their is an exception close the stream and re-throw
+ fs.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a <see cref="FileStream"/> as the entire response body (not attachment)
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">Status code</param>
+ /// <param name="file">The <see cref="FileStream"/> of the desired file to attach</param>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, FileStream file)
+ {
+ //Get content type from filename
+ ContentType ct = HttpHelpers.GetContentTypeFromFile(file.Name);
+ //Set the input as a stream
+ ev.CloseResponse(code, ct, file);
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a character buffer using the server wide
+ /// <see cref="ConnectionInfo.Encoding"/> encoding
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The response status code</param>
+ /// <param name="type">The <see cref="ContentType"/> the data represents</param>
+ /// <param name="data">The character buffer to send</param>
+ /// <remarks>This method will store an encoded copy as a memory stream, so be careful with large buffers</remarks>
+ /// <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)
+ {
+ //Get a memory stream using UTF8 encoding
+ CloseResponse(ev, code, type, in data, ev.Server.Encoding);
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a character buffer using the specified encoding type
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The response status code</param>
+ /// <param name="type">The <see cref="ContentType"/> the data represents</param>
+ /// <param name="data">The character buffer to send</param>
+ /// <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)
+ {
+ if (data.IsEmpty)
+ {
+ ev.CloseResponse(code);
+ return;
+ }
+
+ //Validate encoding
+ _ = encoding ?? throw new ArgumentNullException(nameof(encoding));
+
+ //Get new simple memory response
+ IMemoryResponseReader reader = new SimpleMemoryResponse(data, encoding);
+ ev.CloseResponse(code, type, reader);
+ }
+
+ /// <summary>
+ /// Close a response to a connection by copying the speciifed binary buffer
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The response status code</param>
+ /// <param name="type">The <see cref="ContentType"/> the data represents</param>
+ /// <param name="data">The binary buffer to send</param>
+ /// <remarks>The data paramter is copied into an internal <see cref="IMemoryResponseReader"/></remarks>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan<byte> data)
+ {
+ if (data.IsEmpty)
+ {
+ ev.CloseResponse(code);
+ return;
+ }
+
+ //Get new simple memory response
+ IMemoryResponseReader reader = new SimpleMemoryResponse(data);
+ ev.CloseResponse(code, type, reader);
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a relative file within the current root's directory
+ /// </summary>
+ /// <param name="entity"></param>
+ /// <param name="code">The status code to set the response as</param>
+ /// <param name="filePath">The path of the relative file to send</param>
+ /// <returns>True if the file was found, false if the file does not exist or cannot be accessed</returns>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool CloseWithRelativeFile(this HttpEntity entity, HttpStatusCode code, string filePath)
+ {
+ //See if file exists and is within the root's directory
+ if (entity.RequestedRoot.FindResourceInRoot(filePath, out string realPath))
+ {
+ //get file-info
+ FileInfo realFile = new(realPath);
+ //Close the response with the file stream
+ entity.CloseResponse(code, realFile);
+ return true;
+ }
+ return false;
+ }
+
+ /// <summary>
+ /// Redirects a client using the specified <see cref="RedirectType"/>
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="type">The <see cref="RedirectType"/> redirection type</param>
+ /// <param name="location">Location to direct client to, sets the "Location" header</param>
+ /// <remarks>Sets required headers for redirection, disables cache control, and returns the status code to the client</remarks>
+ /// <exception cref="UriFormatException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Redirect(this IHttpEvent ev, RedirectType type, string location)
+ {
+ Redirect(ev, type, new Uri(location, UriKind.RelativeOrAbsolute));
+ }
+
+ /// <summary>
+ /// Redirects a client using the specified <see cref="RedirectType"/>
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="type">The <see cref="RedirectType"/> redirection type</param>
+ /// <param name="location">Location to direct client to, sets the "Location" header</param>
+ /// <remarks>Sets required headers for redirection, disables cache control, and returns the status code to the client</remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Redirect(this IHttpEvent ev, RedirectType type, Uri location)
+ {
+ //Encode the string for propery http url formatting and set the location header
+ ev.Server.Headers[HttpResponseHeader.Location] = location.ToString();
+ ev.Server.SetNoCache();
+ //Set redirect the ressponse redirect code type
+ ev.CloseResponse((HttpStatusCode)type);
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Attempts to read and deserialize a JSON object from the reqeust body (form data or urlencoded)
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="ev"></param>
+ /// <param name="key">Request argument key (name)</param>
+ /// <param name="obj"></param>
+ /// <returns>true if the argument was found and successfully converted to json</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidJsonRequestException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool TryGetJsonFromArg<T>(this IHttpEvent ev, string key, out T? obj) => TryGetJsonFromArg(ev, key, SR_OPTIONS, out obj);
+
+ /// <summary>
+ /// Attempts to read and deserialize a JSON object from the reqeust body (form data or urlencoded)
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="ev"></param>
+ /// <param name="key">Request argument key (name)</param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during deserialization </param>
+ /// <param name="obj"></param>
+ /// <returns>true if the argument was found and successfully converted to json</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidJsonRequestException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool TryGetJsonFromArg<T>(this IHttpEvent ev, string key, JsonSerializerOptions options, out T? obj)
+ {
+ //Check for key in argument
+ if (ev.RequestArgs.TryGetNonEmptyValue(key, out string? value))
+ {
+ try
+ {
+ //Deserialize and return the object
+ obj = value.AsJsonObject<T>(options);
+ return true;
+ }
+ catch(JsonException je)
+ {
+ throw new InvalidJsonRequestException(je);
+ }
+ }
+ obj = default;
+ return false;
+ }
+
+ /// <summary>
+ /// Reads the value stored at the key location in the request body arguments, into a <see cref="JsonDocument"/>
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="key">Request argument key (name)</param>
+ /// <param name="options"><see cref="JsonDocumentOptions"/> to use during parsing</param>
+ /// <returns>A new <see cref="JsonDocument"/> if the key is found, null otherwise</returns>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="InvalidJsonRequestException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static JsonDocument? GetJsonFromArg(this IHttpEvent ev, string key, in JsonDocumentOptions options = default)
+ {
+ try
+ {
+ //Check for key in argument
+ return ev.RequestArgs.TryGetNonEmptyValue(key, out string? value) ? JsonDocument.Parse(value, options) : null;
+ }
+ catch (JsonException je)
+ {
+ throw new InvalidJsonRequestException(je);
+ }
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) and the file is <see cref="ContentType.Json"/>
+ /// file. It will be deserialzied to the specified object
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="ev"></param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during deserialization </param>
+ /// <returns>Returns the deserialized object if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidJsonRequestException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static T? GetJsonFromFile<T>(this IHttpEvent ev, JsonSerializerOptions? options = null, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return default;
+ }
+
+ FileUpload file = ev.Files[uploadIndex];
+ //Make sure the file is a json file
+ if (file.ContentType != ContentType.Json)
+ {
+ return default;
+ }
+ try
+ {
+ //Beware this will buffer the entire file object before it attmepts to de-serialize it
+ return VnEncoding.JSONDeserializeFromBinary<T>(file.FileData, options);
+ }
+ catch (JsonException je)
+ {
+ throw new InvalidJsonRequestException(je);
+ }
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) and the file is <see cref="ContentType.Json"/>
+ /// file. It will be parsed into a new <see cref="JsonDocument"/>
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <returns>Returns the parsed <see cref="JsonDocument"/>if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidJsonRequestException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static JsonDocument? GetJsonFromFile(this IHttpEvent ev, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return default;
+ }
+ FileUpload file = ev.Files[uploadIndex];
+ //Make sure the file is a json file
+ if (file.ContentType != ContentType.Json)
+ {
+ return default;
+ }
+ try
+ {
+ return JsonDocument.Parse(file.FileData);
+ }
+ catch(JsonException je)
+ {
+ throw new InvalidJsonRequestException(je);
+ }
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) and the file is <see cref="ContentType.Json"/>
+ /// file. It will be deserialzied to the specified object
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="ev"></param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during deserialization </param>
+ /// <returns>The deserialized object if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ /// <exception cref="InvalidJsonRequestException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ValueTask<T?> GetJsonFromFileAsync<T>(this HttpEntity ev, JsonSerializerOptions? options = null, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return ValueTask.FromResult<T?>(default);
+ }
+ FileUpload file = ev.Files[uploadIndex];
+ //Make sure the file is a json file
+ if (file.ContentType != ContentType.Json)
+ {
+ return ValueTask.FromResult<T?>(default);
+ }
+ //avoid copying the ev struct, so return deserialze task
+ static async ValueTask<T?> Deserialze(Stream data, JsonSerializerOptions? options, CancellationToken token)
+ {
+ try
+ {
+ //Beware this will buffer the entire file object before it attmepts to de-serialize it
+ return await VnEncoding.JSONDeserializeFromBinaryAsync<T?>(data, options, token);
+ }
+ catch (JsonException je)
+ {
+ throw new InvalidJsonRequestException(je);
+ }
+ }
+ return Deserialze(file.FileData, options, ev.EventCancellation);
+ }
+
+ static readonly Task<JsonDocument?> DocTaskDefault = Task.FromResult<JsonDocument?>(null);
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) and the file is <see cref="ContentType.Json"/>
+ /// file. It will be parsed into a new <see cref="JsonDocument"/>
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <returns>Returns the parsed <see cref="JsonDocument"/>if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Task<JsonDocument?> GetJsonFromFileAsync(this HttpEntity ev, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return DocTaskDefault;
+ }
+ FileUpload file = ev.Files[uploadIndex];
+ //Make sure the file is a json file
+ if (file.ContentType != ContentType.Json)
+ {
+ return DocTaskDefault;
+ }
+ static async Task<JsonDocument?> Deserialze(Stream data, CancellationToken token)
+ {
+ try
+ {
+ //Beware this will buffer the entire file object before it attmepts to de-serialize it
+ return await JsonDocument.ParseAsync(data, cancellationToken: token);
+ }
+ catch (JsonException je)
+ {
+ throw new InvalidJsonRequestException(je);
+ }
+ }
+ return Deserialze(file.FileData, ev.EventCancellation);
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) the specified parser will be called to parse the
+ /// content body asynchronously into a .net object or its default if no attachments are included
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="parser">A function to asynchronously parse the entity body into its object representation</param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <returns>Returns the parsed <typeparamref name="T"/> if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ public static Task<T?> ParseFileAsAsync<T>(this IHttpEvent ev, Func<Stream, Task<T?>> parser, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return Task.FromResult<T?>(default);
+ }
+ //Get the file
+ FileUpload file = ev.Files[uploadIndex];
+ return parser(file.FileData);
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) the specified parser will be called to parse the
+ /// content body asynchronously into a .net object or its default if no attachments are included
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="parser">A function to asynchronously parse the entity body into its object representation</param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <returns>Returns the parsed <typeparamref name="T"/> if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ public static Task<T?> ParseFileAsAsync<T>(this IHttpEvent ev, Func<Stream, string, Task<T?>> parser, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return Task.FromResult<T?>(default);
+ }
+ //Get the file
+ FileUpload file = ev.Files[uploadIndex];
+ //Parse the file using the specified parser
+ return parser(file.FileData, file.ContentTypeString());
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) the specified parser will be called to parse the
+ /// content body asynchronously into a .net object or its default if no attachments are included
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="parser">A function to asynchronously parse the entity body into its object representation</param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <returns>Returns the parsed <typeparamref name="T"/> if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ public static ValueTask<T?> ParseFileAsAsync<T>(this IHttpEvent ev, Func<Stream, ValueTask<T?>> parser, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return ValueTask.FromResult<T?>(default);
+ }
+ //Get the file
+ FileUpload file = ev.Files[uploadIndex];
+ return parser(file.FileData);
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) the specified parser will be called to parse the
+ /// content body asynchronously into a .net object or its default if no attachments are included
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="parser">A function to asynchronously parse the entity body into its object representation</param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <returns>Returns the parsed <typeparamref name="T"/> if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ public static ValueTask<T?> ParseFileAsAsync<T>(this IHttpEvent ev, Func<Stream, string, ValueTask<T?>> parser, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return ValueTask.FromResult<T?>(default);
+ }
+ //Get the file
+ FileUpload file = ev.Files[uploadIndex];
+ //Parse the file using the specified parser
+ return parser(file.FileData, file.ContentTypeString());
+ }
+
+ /// <summary>
+ /// Gets the bearer token from an authorization header
+ /// </summary>
+ /// <param name="ci"></param>
+ /// <param name="token">The token stored in the user's authorization header</param>
+ /// <returns>True if the authorization header was set, has a Bearer token value</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool HasAuthorization(this IConnectionInfo ci, [NotNullWhen(true)] out string? token)
+ {
+ //Get auth header value
+ string? authorization = ci.Headers[HttpRequestHeader.Authorization];
+ //Check if its set
+ if (!string.IsNullOrWhiteSpace(authorization))
+ {
+ int bearerIndex = authorization.IndexOf(BEARER_STRING, StringComparison.OrdinalIgnoreCase);
+ //Calc token offset, get token, and trim any whitespace
+ token = authorization[(bearerIndex + BEARER_LEN)..].Trim();
+ return true;
+ }
+ token = null;
+ return false;
+ }
+
+ /// <summary>
+ /// Get a <see cref="DirectoryInfo"/> instance that points to the current sites filesystem root.
+ /// </summary>
+ /// <returns></returns>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="PathTooLongException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static DirectoryInfo GetRootDir(this HttpEntity ev) => new(ev.RequestedRoot.Directory);
+
+ /// <summary>
+ /// Returns the MIME string representation of the content type of the uploaded file.
+ /// </summary>
+ /// <param name="upload"></param>
+ /// <returns>The MIME string representation of the content type of the uploaded file.</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string ContentTypeString(this in FileUpload upload) => HttpHelpers.GetContentTypeString(upload.ContentType);
+
+
+ /// <summary>
+ /// Attemts to upgrade the connection to a websocket, if the setup fails, it sets up the response to the client accordingly.
+ /// </summary>
+ /// <param name="entity"></param>
+ /// <param name="socketOpenedcallback">A delegate that will be invoked when the websocket has been opened by the framework</param>
+ /// <param name="subProtocol">The sub-protocol to use on the current websocket</param>
+ /// <param name="userState">An object to store in the <see cref="WebSocketSession.UserState"/> property when the websocket has been accepted</param>
+ /// <returns>True if operation succeeds.</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ public static bool AcceptWebSocket(this IHttpEvent entity, WebsocketAcceptedCallback socketOpenedcallback, object? userState, string? subProtocol = null)
+ {
+ //Make sure this is a websocket request
+ if (!entity.Server.IsWebSocketRequest)
+ {
+ throw new InvalidOperationException("Connection is not a websocket request");
+ }
+
+ //Must define an accept callback
+ _ = socketOpenedcallback ?? throw new ArgumentNullException(nameof(socketOpenedcallback));
+
+ string? version = entity.Server.Headers["Sec-WebSocket-Version"];
+
+ //rfc6455:4.2, version must equal 13
+ if (!string.IsNullOrWhiteSpace(version) && version.Contains("13", StringComparison.OrdinalIgnoreCase))
+ {
+ //Get socket key
+ string? key = entity.Server.Headers["Sec-WebSocket-Key"];
+ if (!string.IsNullOrWhiteSpace(key) && key.Length < 25)
+ {
+ //Set headers for acceptance
+ entity.Server.Headers[HttpResponseHeader.Upgrade] = "websocket";
+ entity.Server.Headers[HttpResponseHeader.Connection] = "Upgrade";
+
+ //Hash accept string
+ entity.Server.Headers["Sec-WebSocket-Accept"] = ManagedHash.ComputeBase64Hash($"{key.Trim()}{HttpHelpers.WebsocketRFC4122Guid}", HashAlg.SHA1);
+
+ //Protocol if user specified it
+ if (!string.IsNullOrWhiteSpace(subProtocol))
+ {
+ entity.Server.Headers["Sec-WebSocket-Protocol"] = subProtocol;
+ }
+
+ //Setup a new websocket session with a new session id
+ entity.DangerousChangeProtocol(new WebSocketSession(subProtocol, socketOpenedcallback)
+ {
+ IsSecure = entity.Server.IsSecure(),
+ UserState = userState
+ });
+
+ return true;
+ }
+ }
+ //Set the client up for a bad request response, nod a valid websocket request
+ entity.CloseResponse(HttpStatusCode.BadRequest);
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/IJsonSerializerBuffer.cs b/Plugins.Essentials/src/Extensions/IJsonSerializerBuffer.cs
new file mode 100644
index 0000000..34811f4
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/IJsonSerializerBuffer.cs
@@ -0,0 +1,48 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: IJsonSerializerBuffer.cs
+*
+* IJsonSerializerBuffer.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.IO;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ /// <summary>
+ /// Interface for a buffer that can be used to serialize objects to JSON
+ /// </summary>
+ interface IJsonSerializerBuffer
+ {
+ /// <summary>
+ /// Gets a stream used for writing serialzation data to
+ /// </summary>
+ /// <returns>The stream to write JSON data to</returns>
+ Stream GetSerialzingStream();
+
+ /// <summary>
+ /// Called when serialization is complete.
+ /// The stream may be inspected for the serialized data.
+ /// </summary>
+ void SerializationComplete();
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs b/Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs
new file mode 100644
index 0000000..3d441a1
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs
@@ -0,0 +1,100 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: InternalSerializerExtensions.cs
+*
+* InternalSerializerExtensions.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 System.IO;
+using System.Text.Json;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+
+ internal static class InternalSerializerExtensions
+ {
+
+ internal static void Serialize<T>(this Utf8JsonWriter writer, IJsonSerializerBuffer buffer, T value, JsonSerializerOptions? options)
+ {
+ //Get stream
+ Stream output = buffer.GetSerialzingStream();
+ try
+ {
+ //Reset writer
+ writer.Reset(output);
+
+ //Serialize
+ JsonSerializer.Serialize(writer, value, options);
+
+ //flush output
+ writer.Flush();
+ }
+ finally
+ {
+ buffer.SerializationComplete();
+ }
+ }
+
+ internal static void Serialize(this Utf8JsonWriter writer, IJsonSerializerBuffer buffer, object value, Type type, JsonSerializerOptions? options)
+ {
+ //Get stream
+ Stream output = buffer.GetSerialzingStream();
+ try
+ {
+ //Reset writer
+ writer.Reset(output);
+
+ //Serialize
+ JsonSerializer.Serialize(writer, value, type, options);
+
+ //flush output
+ writer.Flush();
+ }
+ finally
+ {
+ buffer.SerializationComplete();
+ }
+ }
+
+ internal static void Serialize(this Utf8JsonWriter writer, IJsonSerializerBuffer buffer, JsonDocument document)
+ {
+ //Get stream
+ Stream output = buffer.GetSerialzingStream();
+ try
+ {
+ //Reset writer
+ writer.Reset(output);
+
+ //Serialize
+ document.WriteTo(writer);
+
+ //flush output
+ writer.Flush();
+ }
+ finally
+ {
+ buffer.SerializationComplete();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/InvalidJsonRequestException.cs b/Plugins.Essentials/src/Extensions/InvalidJsonRequestException.cs
new file mode 100644
index 0000000..b2352b2
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/InvalidJsonRequestException.cs
@@ -0,0 +1,57 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: InvalidJsonRequestException.cs
+*
+* InvalidJsonRequestException.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.Text.Json;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ /// <summary>
+ /// Wraps a <see cref="JsonException"/> that is thrown when a JSON request message
+ /// was unsuccessfully parsed.
+ /// </summary>
+ public class InvalidJsonRequestException : JsonException
+ {
+ /// <summary>
+ /// Creates a new <see cref="InvalidJsonRequestException"/> wrapper from a base <see cref="JsonException"/>
+ /// </summary>
+ /// <param name="baseExp"></param>
+ public InvalidJsonRequestException(JsonException baseExp)
+ : base(baseExp.Message, baseExp.Path, baseExp.LineNumber, baseExp.BytePositionInLine, baseExp.InnerException)
+ {
+ base.HelpLink = baseExp.HelpLink;
+ base.Source = baseExp.Source;
+ }
+
+ public InvalidJsonRequestException()
+ {}
+
+ public InvalidJsonRequestException(string message) : base(message)
+ {}
+
+ public InvalidJsonRequestException(string message, System.Exception innerException) : base(message, innerException)
+ {}
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/JsonResponse.cs b/Plugins.Essentials/src/Extensions/JsonResponse.cs
new file mode 100644
index 0000000..22cccd9
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/JsonResponse.cs
@@ -0,0 +1,112 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: JsonResponse.cs
+*
+* JsonResponse.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 System.Buffers;
+using System.IO;
+
+using VNLib.Net.Http;
+using VNLib.Utils.Extensions;
+using VNLib.Utils.IO;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Memory.Caching;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ internal sealed class JsonResponse : IJsonSerializerBuffer, IMemoryResponseReader
+ {
+ private readonly IObjectRental<JsonResponse> _pool;
+
+ private readonly MemoryHandle<byte> _handle;
+ private readonly IMemoryOwner<byte> _memoryOwner;
+ //Stream "owns" the handle, so we cannot dispose the stream
+ private readonly VnMemoryStream _asStream;
+
+ private int _written;
+
+ internal JsonResponse(IObjectRental<JsonResponse> pool)
+ {
+ _pool = pool;
+
+ //Alloc buffer
+ _handle = Memory.Shared.Alloc<byte>(4096, false);
+ //Consume handle for stream, but make sure not to dispose the stream
+ _asStream = VnMemoryStream.ConsumeHandle(_handle, 0, false);
+ //Get memory owner from handle
+ _memoryOwner = _handle.ToMemoryManager(false);
+ }
+
+ ~JsonResponse()
+ {
+ _handle.Dispose();
+ }
+
+ ///<inheritdoc/>
+ public Stream GetSerialzingStream()
+ {
+ //Reset stream position
+ _asStream.Seek(0, SeekOrigin.Begin);
+ return _asStream;
+ }
+
+ ///<inheritdoc/>
+ public void SerializationComplete()
+ {
+ //Reset written position
+ _written = 0;
+ //Update remaining pointer
+ Remaining = Convert.ToInt32(_asStream.Position);
+ }
+
+
+ ///<inheritdoc/>
+ public int Remaining { get; private set; }
+
+ ///<inheritdoc/>
+ void IMemoryResponseReader.Advance(int written)
+ {
+ //Update position
+ _written += written;
+ Remaining -= written;
+ }
+ ///<inheritdoc/>
+ void IMemoryResponseReader.Close()
+ {
+ //Reset and return to pool
+ _written = 0;
+ Remaining = 0;
+ //Return self back to pool
+ _pool.Return(this);
+ }
+
+ ///<inheritdoc/>
+ ReadOnlyMemory<byte> IMemoryResponseReader.GetMemory()
+ {
+ //Get memory from the memory owner and offet the slice,
+ return _memoryOwner.Memory.Slice(_written, Remaining);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/RedirectType.cs b/Plugins.Essentials/src/Extensions/RedirectType.cs
new file mode 100644
index 0000000..eff4d38
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/RedirectType.cs
@@ -0,0 +1,37 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: RedirectType.cs
+*
+* RedirectType.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.Net;
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ /// <summary>
+ /// Shortened list of <see cref="HttpStatusCode"/>s for redirecting connections
+ /// </summary>
+ public enum RedirectType
+ {
+ None,
+ Moved = 301, Found = 302, SeeOther = 303, Temporary = 307, Permanent = 308
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/SimpleMemoryResponse.cs b/Plugins.Essentials/src/Extensions/SimpleMemoryResponse.cs
new file mode 100644
index 0000000..a0f2b17
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/SimpleMemoryResponse.cs
@@ -0,0 +1,89 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: SimpleMemoryResponse.cs
+*
+* SimpleMemoryResponse.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 System.Text;
+using VNLib.Net.Http;
+using System.Buffers;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ internal sealed class SimpleMemoryResponse : IMemoryResponseReader
+ {
+ private byte[]? _buffer;
+ private int _written;
+
+ /// <summary>
+ /// Copies the data in the specified buffer to the internal buffer
+ /// to initalize the new <see cref="SimpleMemoryResponse"/>
+ /// </summary>
+ /// <param name="data">The data to copy</param>
+ public SimpleMemoryResponse(ReadOnlySpan<byte> data)
+ {
+ Remaining = data.Length;
+ //Alloc buffer
+ _buffer = ArrayPool<byte>.Shared.Rent(Remaining);
+ //Copy data to buffer
+ data.CopyTo(_buffer);
+ }
+
+ /// <summary>
+ /// Encodes the character buffer data using the encoder and stores
+ /// the result in the internal buffer for reading.
+ /// </summary>
+ /// <param name="data">The data to encode</param>
+ /// <param name="enc">The encoder to use</param>
+ public SimpleMemoryResponse(ReadOnlySpan<char> data, Encoding enc)
+ {
+ //Calc byte count
+ Remaining = enc.GetByteCount(data);
+
+ //Alloc buffer
+ _buffer = ArrayPool<byte>.Shared.Rent(Remaining);
+
+ //Encode data
+ Remaining = enc.GetBytes(data, _buffer);
+ }
+
+ ///<inheritdoc/>
+ public int Remaining { get; private set; }
+ ///<inheritdoc/>
+ void IMemoryResponseReader.Advance(int written)
+ {
+ Remaining -= written;
+ _written += written;
+ }
+ ///<inheritdoc/>
+ void IMemoryResponseReader.Close()
+ {
+ //Return buffer to pool
+ ArrayPool<byte>.Shared.Return(_buffer!);
+ _buffer = null;
+ }
+ ///<inheritdoc/>
+ ReadOnlyMemory<byte> IMemoryResponseReader.GetMemory() => _buffer!.AsMemory(_written, Remaining);
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/UserExtensions.cs b/Plugins.Essentials/src/Extensions/UserExtensions.cs
new file mode 100644
index 0000000..9223b1d
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/UserExtensions.cs
@@ -0,0 +1,94 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: UserExtensions.cs
+*
+* UserExtensions.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 System.Text.Json;
+
+using VNLib.Plugins.Essentials.Users;
+using VNLib.Plugins.Essentials.Accounts;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ /// <summary>
+ /// Provides extension methods to the Users namespace
+ /// </summary>
+ public static class UserExtensions
+ {
+
+ private const string PROFILE_ENTRY = "__.prof";
+
+ /// <summary>
+ /// Stores the user's profile to their entry.
+ /// <br/>
+ /// NOTE: You must validate/filter data before storing
+ /// </summary>
+ /// <param name="ud"></param>
+ /// <param name="profile">The profile object to store on account</param>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static void SetProfile(this IUser ud, AccountData? profile)
+ {
+ //Clear entry if its null
+ if (profile == null)
+ {
+ ud[PROFILE_ENTRY] = null!;
+ return;
+ }
+ //Dont store duplicate values
+ profile.Created = null;
+ profile.EmailAddress = null;
+ ud.SetObject(PROFILE_ENTRY, profile);
+ }
+ /// <summary>
+ /// Stores the serialized string user's profile to their entry.
+ /// <br/>
+ /// NOTE: No data validation checks are performed
+ /// </summary>
+ /// <param name="ud"></param>
+ /// <param name="jsonProfile">The JSON serialized "raw" profile data</param>
+ public static void SetProfile(this IUser ud, string jsonProfile) => ud[PROFILE_ENTRY] = jsonProfile;
+ /// <summary>
+ /// Recovers the user's stored profile
+ /// </summary>
+ /// <returns>The user's profile stored in the entry or null if no entry is found</returns>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static AccountData? GetProfile(this IUser ud)
+ {
+ //Recover profile data, or create new empty profile data
+ AccountData? ad = ud.GetObject<AccountData>(PROFILE_ENTRY);
+ if (ad == null)
+ {
+ return null;
+ }
+ //Set email the same as the account
+ ad.EmailAddress = ud.EmailAddress;
+ //Store the rfc time
+ ad.Created = ud.Created.ToString("R");
+ return ad;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/FileProcessArgs.cs b/Plugins.Essentials/src/FileProcessArgs.cs
new file mode 100644
index 0000000..dae695b
--- /dev/null
+++ b/Plugins.Essentials/src/FileProcessArgs.cs
@@ -0,0 +1,169 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: FileProcessArgs.cs
+*
+* FileProcessArgs.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 System.Net;
+
+namespace VNLib.Plugins.Essentials
+{
+ /// <summary>
+ /// Server routine to follow after processing selector
+ /// </summary>
+ public enum FpRoutine
+ {
+ /// <summary>
+ /// There was an error during processing and the server should immediatly respond with a <see cref="HttpStatusCode.InternalServerError"/> error code
+ /// </summary>
+ Error,
+ /// <summary>
+ /// The server should continue the file read operation with the current information
+ /// </summary>
+ Continue,
+ /// <summary>
+ /// The server should redirect the conneciton to an alternate location
+ /// </summary>
+ Redirect,
+ /// <summary>
+ /// The server should immediatly respond with a <see cref="HttpStatusCode.Forbidden"/> error code
+ /// </summary>
+ Deny,
+ /// <summary>
+ /// The server should fulfill the reqeest by sending the contents of an alternate file location (if it exists) with the existing connection
+ /// </summary>
+ ServeOther,
+ /// <summary>
+ /// The server should immediatly respond with a <see cref="HttpStatusCode.NotFound"/> error code
+ /// </summary>
+ NotFound,
+ /// <summary>
+ /// Serves another file location that must be a trusted fully qualified location
+ /// </summary>
+ ServeOtherFQ,
+ /// <summary>
+ /// The connection does not require a file to be processed
+ /// </summary>
+ VirtualSkip,
+ }
+
+ /// <summary>
+ /// Specifies operations the file processor will follow during request handling
+ /// </summary>
+ public readonly struct FileProcessArgs : IEquatable<FileProcessArgs>
+ {
+ /// <summary>
+ /// Signals the file processor should complete with a <see cref="FpRoutine.Deny"/> routine
+ /// </summary>
+ public static readonly FileProcessArgs Deny = new (FpRoutine.Deny);
+ /// <summary>
+ /// Signals the file processor should continue with intended/normal processing of the request
+ /// </summary>
+ public static readonly FileProcessArgs Continue = new (FpRoutine.Continue);
+ /// <summary>
+ /// Signals the file processor should complete with a <see cref="FpRoutine.Error"/> routine
+ /// </summary>
+ public static readonly FileProcessArgs Error = new (FpRoutine.Error);
+ /// <summary>
+ /// Signals the file processor should complete with a <see cref="FpRoutine.NotFound"/> routine
+ /// </summary>
+ public static readonly FileProcessArgs NotFound = new (FpRoutine.NotFound);
+ /// <summary>
+ /// Signals the file processor should not process the connection
+ /// </summary>
+ public static readonly FileProcessArgs VirtualSkip = new (FpRoutine.VirtualSkip);
+ /// <summary>
+ /// The routine the file processor should execute
+ /// </summary>
+ public readonly FpRoutine Routine { get; init; }
+ /// <summary>
+ /// An optional alternate path for the given routine
+ /// </summary>
+ public readonly string Alternate { get; init; }
+
+ /// <summary>
+ /// Initializes a new <see cref="FileProcessArgs"/> with the specified routine
+ /// and empty <see cref="Alternate"/> path
+ /// </summary>
+ /// <param name="routine">The file processing routine to execute</param>
+ public FileProcessArgs(FpRoutine routine)
+ {
+ this.Routine = routine;
+ this.Alternate = string.Empty;
+ }
+ /// <summary>
+ /// Initializes a new <see cref="FileProcessArgs"/> with the specified routine
+ /// and alternate path
+ /// </summary>
+ /// <param name="routine"></param>
+ /// <param name="alternatePath"></param>
+ public FileProcessArgs(FpRoutine routine, string alternatePath)
+ {
+ this.Routine = routine;
+ this.Alternate = alternatePath;
+ }
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="arg1"></param>
+ /// <param name="arg2"></param>
+ /// <returns></returns>
+ public static bool operator == (FileProcessArgs arg1, FileProcessArgs arg2)
+ {
+ return arg1.Equals(arg2);
+ }
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="arg1"></param>
+ /// <param name="arg2"></param>
+ /// <returns></returns>
+ public static bool operator != (FileProcessArgs arg1, FileProcessArgs arg2)
+ {
+ return !arg1.Equals(arg2);
+ }
+ ///<inheritdoc/>
+ public bool Equals(FileProcessArgs other)
+ {
+ //make sure the routine types match
+ if (Routine != other.Routine)
+ {
+ return false;
+ }
+ //Next make sure the hashcodes of the alternate paths match
+ return (Alternate?.GetHashCode(StringComparison.OrdinalIgnoreCase)) == (other.Alternate?.GetHashCode(StringComparison.OrdinalIgnoreCase));
+ }
+ ///<inheritdoc/>
+ public override bool Equals(object obj)
+ {
+ return obj is FileProcessArgs args && Equals(args);
+ }
+ /// <summary>
+ /// <inheritdoc/>
+ /// </summary>
+ /// <returns></returns>
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/HttpEntity.cs b/Plugins.Essentials/src/HttpEntity.cs
new file mode 100644
index 0000000..ffad607
--- /dev/null
+++ b/Plugins.Essentials/src/HttpEntity.cs
@@ -0,0 +1,178 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: HttpEntity.cs
+*
+* HttpEntity.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 System.IO;
+using System.Net;
+using System.Threading;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+using VNLib.Net.Http;
+using VNLib.Plugins.Essentials.Sessions;
+using VNLib.Plugins.Essentials.Extensions;
+
+#nullable enable
+
+/*
+ * HttpEntity was converted to an object as during profiling
+ * it was almost always heap allcated due to async opertaions
+ * or other object tracking issues. So to reduce the number of
+ * allocations (at the cost of larger objects) basic profiling
+ * showed less GC load and less collections when SessionInfo
+ * remained a value type
+ */
+#pragma warning disable CA1051 // Do not declare visible instance fields
+
+namespace VNLib.Plugins.Essentials
+{
+ /// <summary>
+ /// A container for an <see cref="HttpEvent"/> with its attached session.
+ /// This class cannot be inherited.
+ /// </summary>
+ public sealed class HttpEntity : IHttpEvent
+ {
+ /// <summary>
+ /// The connection event entity
+ /// </summary>
+ private readonly IHttpEvent Entity;
+ public HttpEntity(IHttpEvent entity, EventProcessor root, in SessionHandle session, in CancellationToken cancellation)
+ {
+ Entity = entity;
+ RequestedRoot = root;
+ EventCancellation = cancellation;
+
+ //See if the connection is coming from an downstream server
+ IsBehindDownStreamServer = root.Options.DownStreamServers.Contains(entity.Server.RemoteEndpoint.Address);
+ /*
+ * If the connection was behind a trusted downstream server,
+ * we can trust the x-forwarded-for header,
+ * otherwise use the remote ep ip address
+ */
+ TrustedRemoteIp = entity.Server.GetTrustedIp(IsBehindDownStreamServer);
+ //Initialize the session
+ Session = session.IsSet ? new(session.SessionData, entity.Server, TrustedRemoteIp) : new();
+ //Local connection
+ IsLocalConnection = entity.Server.LocalEndpoint.Address.IsLocalSubnet(TrustedRemoteIp);
+ //Cache value
+ IsSecure = entity.Server.IsSecure(IsBehindDownStreamServer);
+ }
+
+ /// <summary>
+ /// A token that has a scheduled timeout to signal the cancellation of the entity event
+ /// </summary>
+ public readonly CancellationToken EventCancellation;
+ /// <summary>
+ /// The session assocaited with the event
+ /// </summary>
+ public readonly SessionInfo Session;
+ /// <summary>
+ /// A value that indicates if the connecion came from a trusted downstream server
+ /// </summary>
+ public readonly bool IsBehindDownStreamServer;
+ /// <summary>
+ /// Determines if the connection came from the local network to the current server
+ /// </summary>
+ public readonly bool IsLocalConnection;
+ /// <summary>
+ /// Gets a value that determines if the connection is using tls, locally
+ /// or behind a trusted downstream server that is using tls.
+ /// </summary>
+ public readonly bool IsSecure;
+
+ /// <summary>
+ /// The connection info object assocated with the entity
+ /// </summary>
+ public IConnectionInfo Server => Entity.Server;
+ /// <summary>
+ /// User's ip. If the connection is behind a local proxy, returns the users actual IP. Otherwise returns the connection ip.
+ /// </summary>
+ public readonly IPAddress TrustedRemoteIp;
+ /// <summary>
+ /// The requested web root. Provides additional site information
+ /// </summary>
+ public readonly EventProcessor RequestedRoot;
+ /// <summary>
+ /// If the request has query arguments they are stored in key value format
+ /// </summary>
+ public IReadOnlyDictionary<string, string> QueryArgs => Entity.QueryArgs;
+ /// <summary>
+ /// If the request body has form data or url encoded arguments they are stored in key value format
+ /// </summary>
+ public IReadOnlyDictionary<string, string> RequestArgs => Entity.RequestArgs;
+ /// <summary>
+ /// Contains all files upladed with current request
+ /// </summary>
+ public IReadOnlyList<FileUpload> Files => Entity.Files;
+ ///<inheritdoc/>
+ HttpServer IHttpEvent.OriginServer => Entity.OriginServer;
+
+ /// <summary>
+ /// Complete the session and respond to user
+ /// </summary>
+ /// <param name="code">Status code of operation</param>
+ /// <exception cref="InvalidOperationException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void CloseResponse(HttpStatusCode code) => Entity.CloseResponse(code);
+
+ ///<inheritdoc/>
+ ///<exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void CloseResponse(HttpStatusCode code, ContentType type, Stream stream)
+ {
+ Entity.CloseResponse(code, type, stream);
+ //Verify content type matches
+ if (!Server.Accepts(type))
+ {
+ throw new ContentTypeUnacceptableException("The client does not accept the content type of the response");
+ }
+ }
+
+ ///<inheritdoc/>
+ ///<exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void CloseResponse(HttpStatusCode code, ContentType type, IMemoryResponseReader entity)
+ {
+ //Verify content type matches
+ if (!Server.Accepts(type))
+ {
+ throw new ContentTypeUnacceptableException("The client does not accept the content type of the response");
+ }
+
+ Entity.CloseResponse(code, type, entity);
+ }
+
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void DisableCompression() => Entity.DisableCompression();
+
+ /*
+ * Do not directly expose dangerous methods, but allow them to be called
+ */
+
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ void IHttpEvent.DangerousChangeProtocol(IAlternateProtocol protocolHandler) => Entity.DangerousChangeProtocol(protocolHandler);
+ }
+}
diff --git a/Plugins.Essentials/src/IEpProcessingOptions.cs b/Plugins.Essentials/src/IEpProcessingOptions.cs
new file mode 100644
index 0000000..de79327
--- /dev/null
+++ b/Plugins.Essentials/src/IEpProcessingOptions.cs
@@ -0,0 +1,66 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: IEpProcessingOptions.cs
+*
+* IEpProcessingOptions.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 System.IO;
+using System.Net;
+using System.Collections.Generic;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials
+{
+ /// <summary>
+ /// Provides an interface for <see cref="EventProcessor"/>
+ /// security options
+ /// </summary>
+ public interface IEpProcessingOptions
+ {
+ /// <summary>
+ /// The name of a default file to search for within a directory if no file is specified (index.html).
+ /// This array should be ordered.
+ /// </summary>
+ IReadOnlyCollection<string> DefaultFiles { get; }
+ /// <summary>
+ /// File extensions that are denied from being read from the filesystem
+ /// </summary>
+ IReadOnlySet<string> ExcludedExtensions { get; }
+ /// <summary>
+ /// File attributes that must be matched for the file to be accessed
+ /// </summary>
+ FileAttributes AllowedAttributes { get; }
+ /// <summary>
+ /// Files that match any attribute flag set will be denied
+ /// </summary>
+ FileAttributes DissallowedAttributes { get; }
+ /// <summary>
+ /// A table of known downstream servers/ports that can be trusted to proxy connections
+ /// </summary>
+ IReadOnlySet<IPAddress> DownStreamServers { get; }
+ /// <summary>
+ /// A <see cref="TimeSpan"/> for how long a connection may remain open before all operations are cancelled
+ /// </summary>
+ TimeSpan ExecutionTimeout { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Oauth/IOAuth2Provider.cs b/Plugins.Essentials/src/Oauth/IOAuth2Provider.cs
new file mode 100644
index 0000000..30944b8
--- /dev/null
+++ b/Plugins.Essentials/src/Oauth/IOAuth2Provider.cs
@@ -0,0 +1,44 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: IOAuth2Provider.cs
+*
+* IOAuth2Provider.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 System.Threading.Tasks;
+
+using VNLib.Plugins.Essentials.Sessions;
+
+namespace VNLib.Plugins.Essentials.Oauth
+{
+ /// <summary>
+ /// An interface that Oauth2 serice providers must implement
+ /// to provide sessions to an <see cref="EventProcessor"/>
+ /// processor endpoint processor
+ /// </summary>
+ public interface IOAuth2Provider : ISessionProvider
+ {
+ /// <summary>
+ /// Gets a value indicating how long a session may be valid for
+ /// </summary>
+ public TimeSpan MaxTokenLifetime { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Oauth/O2EndpointBase.cs b/Plugins.Essentials/src/Oauth/O2EndpointBase.cs
new file mode 100644
index 0000000..a1a4d35
--- /dev/null
+++ b/Plugins.Essentials/src/Oauth/O2EndpointBase.cs
@@ -0,0 +1,162 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: O2EndpointBase.cs
+*
+* O2EndpointBase.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 System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+
+using VNLib.Utils;
+using VNLib.Utils.Logging;
+using VNLib.Net.Http;
+using VNLib.Plugins.Essentials.Extensions;
+using VNLib.Plugins.Essentials.Endpoints;
+
+namespace VNLib.Plugins.Essentials.Oauth
+{
+ /// <summary>
+ /// An base class for HttpEntity processors (endpoints) for processing
+ /// Oauth2 client requests. Similar to <seealso cref="ProtectedWebEndpoint"/>
+ /// but for Oauth2 sessions
+ /// </summary>
+ public abstract class O2EndpointBase : ResourceEndpointBase
+ {
+ //Disable browser only protection
+ ///<inheritdoc/>
+ protected override ProtectionSettings EndpointProtectionSettings { get; } = new() { DisableBrowsersOnly = true };
+
+ ///<inheritdoc/>
+ public override async ValueTask<VfReturnType> Process(HttpEntity entity)
+ {
+ try
+ {
+ VfReturnType rt;
+ ERRNO preProc = PreProccess(entity);
+ //Entity was responded to by the pre-processor
+ if (preProc < 0)
+ {
+ return VfReturnType.VirtualSkip;
+ }
+ if (preProc == ERRNO.E_FAIL)
+ {
+ rt = VfReturnType.Forbidden;
+ goto Exit;
+ }
+ //If websockets are quested allow them to be processed in a logged-in/secure context
+ if (entity.Server.IsWebSocketRequest)
+ {
+ return await WebsocketRequestedAsync(entity);
+ }
+ //Capture return type
+ rt = entity.Server.Method switch
+ {
+ //Get request to get account
+ HttpMethod.GET => await GetAsync(entity),
+ HttpMethod.POST => await PostAsync(entity),
+ HttpMethod.DELETE => await DeleteAsync(entity),
+ HttpMethod.PUT => await PutAsync(entity),
+ HttpMethod.PATCH => await PatchAsync(entity),
+ HttpMethod.OPTIONS => await OptionsAsync(entity),
+ _ => await AlternateMethodAsync(entity, entity.Server.Method),
+ };
+ Exit:
+ //Write a standard Ouath2 error messag
+ switch (rt)
+ {
+ case VfReturnType.VirtualSkip:
+ return VfReturnType.VirtualSkip;
+ case VfReturnType.ProcessAsFile:
+ return VfReturnType.ProcessAsFile;
+ case VfReturnType.NotFound:
+ entity.CloseResponseError(HttpStatusCode.NotFound, ErrorType.InvalidRequest, "The requested resource could not be found");
+ return VfReturnType.VirtualSkip;
+ case VfReturnType.BadRequest:
+ entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidRequest, "Your request was not properlty formatted and could not be proccessed");
+ return VfReturnType.VirtualSkip;
+ case VfReturnType.Error:
+ entity.CloseResponseError(HttpStatusCode.InternalServerError, ErrorType.ServerError, "There was a server error processing your request");
+ return VfReturnType.VirtualSkip;
+
+ case VfReturnType.Forbidden:
+ default:
+ entity.CloseResponseError(HttpStatusCode.Forbidden, ErrorType.InvalidClient, "You do not have access to this resource");
+ return VfReturnType.VirtualSkip;
+ }
+ }
+ catch (TerminateConnectionException)
+ {
+ //A TC exception is intentional and should always propagate to the runtime
+ throw;
+ }
+ //Re-throw exceptions that are cause by reading the transport layer
+ catch (IOException ioe) when (ioe.InnerException is SocketException)
+ {
+ throw;
+ }
+ catch (ContentTypeUnacceptableException)
+ {
+ //Respond with an 406 error message
+ entity.CloseResponseError(HttpStatusCode.NotAcceptable, ErrorType.InvalidRequest, "The response type is not acceptable for this endpoint");
+ return VfReturnType.VirtualSkip;
+ }
+ catch (InvalidJsonRequestException)
+ {
+ //Respond with an error message
+ entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidRequest, "The request body was not a proper JSON schema");
+ return VfReturnType.VirtualSkip;
+ }
+ catch (Exception ex)
+ {
+ //Log an uncaught excetpion and return an error code (log may not be initialized)
+ Log?.Error(ex);
+ //Respond with an error message
+ entity.CloseResponseError(HttpStatusCode.InternalServerError, ErrorType.ServerError, "There was a server error processing your request");
+ return VfReturnType.VirtualSkip;
+ }
+ }
+
+ /// <summary>
+ /// Runs base pre-processing and ensures "sessions" OAuth2 token
+ /// session is loaded
+ /// </summary>
+ /// <param name="entity">The request entity to process</param>
+ /// <inheritdoc/>
+ protected override ERRNO PreProccess(HttpEntity entity)
+ {
+ //Make sure session is loaded (token is valid)
+ if (!entity.Session.IsSet)
+ {
+ entity.CloseResponseError(HttpStatusCode.Forbidden, ErrorType.InvalidToken, "Your token is not valid");
+ return -1;
+ }
+ //Must be an oauth session
+ if (entity.Session.SessionType != Sessions.SessionType.OAuth2)
+ {
+ return false;
+ }
+ return base.PreProccess(entity);
+ }
+ }
+}
diff --git a/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs b/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs
new file mode 100644
index 0000000..892a24c
--- /dev/null
+++ b/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs
@@ -0,0 +1,239 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: OauthHttpExtensions.cs
+*
+* OauthHttpExtensions.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.Net;
+using System.Text;
+
+using VNLib.Net.Http;
+using VNLib.Utils;
+using VNLib.Utils.IO;
+using VNLib.Utils.Memory.Caching;
+using VNLib.Plugins.Essentials.Extensions;
+
+namespace VNLib.Plugins.Essentials.Oauth
+{
+ /// <summary>
+ /// An OAuth2 specification error code
+ /// </summary>
+ public enum ErrorType
+ {
+ /// <summary>
+ /// The request is considered invalid and cannot be continued
+ /// </summary>
+ InvalidRequest,
+ /// <summary>
+ ///
+ /// </summary>
+ InvalidClient,
+ /// <summary>
+ /// The supplied token is no longer considered valid
+ /// </summary>
+ InvalidToken,
+ /// <summary>
+ /// The token does not have the authorization required, is missing authorization, or is no longer considered acceptable
+ /// </summary>
+ UnauthorizedClient,
+ /// <summary>
+ /// The client accept content type is unacceptable for the requested endpoint and cannot be processed
+ /// </summary>
+ UnsupportedResponseType,
+ /// <summary>
+ /// The scope of the token does not allow for this operation
+ /// </summary>
+ InvalidScope,
+ /// <summary>
+ /// There was a server related error and the request could not be fulfilled
+ /// </summary>
+ ServerError,
+ /// <summary>
+ /// The request could not be processed at this time
+ /// </summary>
+ TemporarilyUnabavailable
+ }
+
+ 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>
+ /// <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 HttpEntity 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\":\"");
+ 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\":\"");
+ 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("\"}");
+
+ 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);
+ }
+ //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);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Oauth/OauthSessionCacheExhaustedException.cs b/Plugins.Essentials/src/Oauth/OauthSessionCacheExhaustedException.cs
new file mode 100644
index 0000000..da91444
--- /dev/null
+++ b/Plugins.Essentials/src/Oauth/OauthSessionCacheExhaustedException.cs
@@ -0,0 +1,43 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: OauthSessionCacheExhaustedException.cs
+*
+* OauthSessionCacheExhaustedException.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;
+
+
+namespace VNLib.Plugins.Essentials.Oauth
+{
+ /// <summary>
+ /// Raised when the session cache space has been exhausted and cannot
+ /// load the new session into cache.
+ /// </summary>
+ public class OauthSessionCacheExhaustedException : Exception
+ {
+ public OauthSessionCacheExhaustedException(string message) : base(message)
+ {}
+ public OauthSessionCacheExhaustedException(string message, Exception innerException) : base(message, innerException)
+ {}
+ public OauthSessionCacheExhaustedException()
+ {}
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Oauth/OauthSessionExtensions.cs b/Plugins.Essentials/src/Oauth/OauthSessionExtensions.cs
new file mode 100644
index 0000000..6f9d275
--- /dev/null
+++ b/Plugins.Essentials/src/Oauth/OauthSessionExtensions.cs
@@ -0,0 +1,88 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: OauthSessionExtensions.cs
+*
+* OauthSessionExtensions.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.Plugins.Essentials.Sessions;
+
+namespace VNLib.Plugins.Essentials.Oauth
+{
+ /// <summary>
+ /// Represents an active oauth session
+ /// </summary>
+ public static class OauthSessionExtensions
+ {
+ public const string APP_ID_ENTRY = "oau.apid";
+ public const string REFRESH_TOKEN_ENTRY = "oau.rt";
+ public const string SCOPES_ENTRY = "oau.scp";
+ public const string TOKEN_TYPE_ENTRY = "oau.typ";
+
+
+ /// <summary>
+ /// The ID of the application that granted the this token access
+ /// </summary>
+ public static string AppID(this in SessionInfo session) => session[APP_ID_ENTRY];
+
+ /// <summary>
+ /// The refresh token for this current token
+ /// </summary>
+ public static string RefreshToken(this in SessionInfo session) => session[REFRESH_TOKEN_ENTRY];
+
+ /// <summary>
+ /// The token's privilage scope
+ /// </summary>
+ public static string Scopes(this in SessionInfo session) => session[SCOPES_ENTRY];
+ /// <summary>
+ /// The Oauth2 token type
+ /// </summary>,
+ public static string Type(this in SessionInfo session) => session[TOKEN_TYPE_ENTRY];
+
+ /// <summary>
+ /// Determines if the current session has the required scope type and the
+ /// specified permission
+ /// </summary>
+ /// <param name="session"></param>
+ /// <param name="type">The scope type</param>
+ /// <param name="permission">The scope permission</param>
+ /// <returns>True if the current session has the required scope, false otherwise</returns>
+ public static bool HasScope(this in SessionInfo session, string type, string permission)
+ {
+ //Join the permissions components
+ string perms = string.Concat(type, ":", permission);
+ return session.HasScope(perms);
+ }
+ /// <summary>
+ /// Determines if the current session has the required scope type and the
+ /// specified permission
+ /// </summary>
+ /// <param name="session"></param>
+ /// <param name="scope">The scope to compare</param>
+ /// <returns>True if the current session has the required scope, false otherwise</returns>
+ public static bool HasScope(this in SessionInfo session, ReadOnlySpan<char> scope)
+ {
+ //Split the scope components and check them against the joined permission
+ return session.Scopes().AsSpan().Contains(scope, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Sessions/ISession.cs b/Plugins.Essentials/src/Sessions/ISession.cs
new file mode 100644
index 0000000..e15c6e2
--- /dev/null
+++ b/Plugins.Essentials/src/Sessions/ISession.cs
@@ -0,0 +1,94 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: ISession.cs
+*
+* ISession.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 System.Net;
+
+using VNLib.Utils;
+
+namespace VNLib.Plugins.Essentials.Sessions
+{
+ /// <summary>
+ /// Flags to specify <see cref="ISession"/> session types
+ /// </summary>
+ public enum SessionType
+ {
+ /// <summary>
+ /// The session is a "basic" or web based session
+ /// </summary>
+ Web,
+ /// <summary>
+ /// The session is an OAuth2 session type
+ /// </summary>
+ OAuth2
+ }
+
+ /// <summary>
+ /// Represents a connection oriented session data
+ /// </summary>
+ public interface ISession : IIndexable<string, string>
+ {
+ /// <summary>
+ /// A value specifying the type of the loaded session
+ /// </summary>
+ SessionType SessionType { get; }
+ /// <summary>
+ /// UTC time in when the session was created
+ /// </summary>
+ DateTimeOffset Created { get; }
+ /// <summary>
+ /// Privilages associated with user specified during login
+ /// </summary>
+ ulong Privilages { get; set; }
+ /// <summary>
+ /// Key that identifies the current session. (Identical to cookie::sessionid)
+ /// </summary>
+ string SessionID { get; }
+ /// <summary>
+ /// User ID associated with session
+ /// </summary>
+ string UserID { get; set; }
+ /// <summary>
+ /// Marks the session as invalid
+ /// </summary>
+ void Invalidate(bool all = false);
+ /// <summary>
+ /// Gets or sets the session's authorization token
+ /// </summary>
+ string Token { get; set; }
+ /// <summary>
+ /// The IP address belonging to the client
+ /// </summary>
+ IPAddress UserIP { get; }
+ /// <summary>
+ /// Sets the session ID to be regenerated if applicable
+ /// </summary>
+ void RegenID();
+
+ /// <summary>
+ /// A value that indicates this session was newly created
+ /// </summary>
+ bool IsNew { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Sessions/ISessionExtensions.cs b/Plugins.Essentials/src/Sessions/ISessionExtensions.cs
new file mode 100644
index 0000000..44063f9
--- /dev/null
+++ b/Plugins.Essentials/src/Sessions/ISessionExtensions.cs
@@ -0,0 +1,95 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: ISessionExtensions.cs
+*
+* ISessionExtensions.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 System.Runtime.CompilerServices;
+using System.Security.Authentication;
+
+using VNLib.Net.Http;
+using VNLib.Utils;
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Plugins.Essentials.Sessions
+{
+ public static class ISessionExtensions
+ {
+ public const string USER_AGENT_ENTRY = "__.ua";
+ public const string ORIGIN_ENTRY = "__.org";
+ public const string REFER_ENTRY = "__.rfr";
+ public const string SECURE_ENTRY = "__.sec";
+ public const string CROSS_ORIGIN = "__.cor";
+ public const string LOCAL_TIME_ENTRY = "__.lot";
+ public const string LOGIN_TOKEN_ENTRY = "__.lte";
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string GetUserAgent(this ISession session) => session[USER_AGENT_ENTRY];
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void SetUserAgent(this ISession session, string userAgent) => session[USER_AGENT_ENTRY] = userAgent;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string GetOrigin(this ISession session) => session[ORIGIN_ENTRY];
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Uri GetOriginUri(this ISession session) => Uri.TryCreate(session[ORIGIN_ENTRY], UriKind.Absolute, out Uri origin) ? origin : null;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void SetOrigin(this ISession session, string origin) => session[ORIGIN_ENTRY] = origin;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string GetRefer(this ISession session) => session[REFER_ENTRY];
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void SetRefer(this ISession session, string refer) => session[REFER_ENTRY] = refer;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static SslProtocols GetSecurityProtocol(this ISession session)
+ {
+ return (SslProtocols)session.GetValueType<string, int>(SECURE_ENTRY);
+ }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void SetSecurityProtocol(this ISession session, SslProtocols protocol) => session[SECURE_ENTRY] = VnEncoding.ToBase32String((int)protocol);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsCrossOrigin(this ISession session) => session[CROSS_ORIGIN] == bool.TrueString;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void IsCrossOrigin(this ISession session, bool crossOrign) => session[CROSS_ORIGIN] = crossOrign.ToString();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string GetLoginToken(this ISession session) => session[LOGIN_TOKEN_ENTRY];
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void SetLoginToken(this ISession session, string loginToken) => session[LOGIN_TOKEN_ENTRY] = loginToken;
+
+ /// <summary>
+ /// Initializes a "new" session with initial varaibles from the current connection
+ /// for lookup/comparison later
+ /// </summary>
+ /// <param name="session"></param>
+ /// <param name="ci">The <see cref="ConnectionInfo"/> object containing connection details</param>
+ public static void InitNewSession(this ISession session, IConnectionInfo ci)
+ {
+ session.IsCrossOrigin(ci.CrossOrigin);
+ session.SetOrigin(ci.Origin?.ToString());
+ session.SetRefer(ci.Referer?.ToString());
+ session.SetSecurityProtocol(ci.SecurityProtocol);
+ session.SetUserAgent(ci.UserAgent);
+ }
+
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Sessions/ISessionProvider.cs b/Plugins.Essentials/src/Sessions/ISessionProvider.cs
new file mode 100644
index 0000000..fe7e7ce
--- /dev/null
+++ b/Plugins.Essentials/src/Sessions/ISessionProvider.cs
@@ -0,0 +1,49 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: ISessionProvider.cs
+*
+* ISessionProvider.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 System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Net.Http;
+
+namespace VNLib.Plugins.Essentials.Sessions
+{
+ /// <summary>
+ /// Provides stateful session objects assocated with HTTP connections
+ /// </summary>
+ public interface ISessionProvider
+ {
+ /// <summary>
+ /// Gets a session handle for the current connection
+ /// </summary>
+ /// <param name="entity">The connection to get associated session on</param>
+ /// <param name="cancellationToken"></param>
+ /// <returns>A task the resolves an <see cref="SessionHandle"/> instance</returns>
+ /// <exception cref="TimeoutException"></exception>
+ /// <exception cref="SessionException"></exception>
+ /// <exception cref="OperationCanceledException"></exception>
+ public ValueTask<SessionHandle> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken);
+ }
+}
diff --git a/Plugins.Essentials/src/Sessions/SessionBase.cs b/Plugins.Essentials/src/Sessions/SessionBase.cs
new file mode 100644
index 0000000..d386b8b
--- /dev/null
+++ b/Plugins.Essentials/src/Sessions/SessionBase.cs
@@ -0,0 +1,168 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: SessionBase.cs
+*
+* SessionBase.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 System.Net;
+using System.Runtime.CompilerServices;
+
+using VNLib.Net.Http;
+using VNLib.Utils;
+using VNLib.Utils.Async;
+
+namespace VNLib.Plugins.Essentials.Sessions
+{
+ /// <summary>
+ /// 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
+ {
+ protected const ulong MODIFIED_MSK = 0b0000000000000001UL;
+ protected const ulong IS_NEW_MSK = 0b0000000000000010UL;
+ protected const ulong REGEN_ID_MSK = 0b0000000000000100UL;
+ protected const ulong INVALID_MSK = 0b0000000000001000UL;
+ protected const ulong ALL_INVALID_MSK = 0b0000000000100000UL;
+
+ protected const string USER_ID_ENTRY = "__.i.uid";
+ protected const string TOKEN_ENTRY = "__.i.tk";
+ protected const string PRIV_ENTRY = "__.i.pl";
+ protected const string IP_ADDRESS_ENTRY = "__.i.uip";
+ protected const string SESSION_TYPE_ENTRY = "__.i.tp";
+
+ /// <summary>
+ /// A <see cref="BitField"/> of status flags for the state of the current session.
+ /// May be used internally
+ /// </summary>
+ protected BitField Flags { get; } = new(0);
+
+ /// <summary>
+ /// Gets or sets the Modified flag
+ /// </summary>
+ protected bool IsModified
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => Flags.IsSet(MODIFIED_MSK);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set => Flags.Set(MODIFIED_MSK, value);
+ }
+
+ ///<inheritdoc/>
+ public virtual string SessionID { get; protected set; }
+ ///<inheritdoc/>
+ public virtual DateTimeOffset Created { get; protected set; }
+
+ ///<inheritdoc/>
+ ///<exception cref="ObjectDisposedException"></exception>
+ public string this[string index]
+ {
+ get
+ {
+ Check();
+ return IndexerGet(index);
+ }
+ set
+ {
+ Check();
+ IndexerSet(index, value);
+ }
+ }
+ ///<inheritdoc/>
+ public virtual IPAddress UserIP
+ {
+ get
+ {
+ //try to parse the IP address, otherwise return null
+ _ = IPAddress.TryParse(this[IP_ADDRESS_ENTRY], out IPAddress ip);
+ return ip;
+ }
+ protected set
+ {
+ //Store the IP address as its string representation
+ this[IP_ADDRESS_ENTRY] = value?.ToString();
+ }
+ }
+ ///<inheritdoc/>
+ public virtual SessionType SessionType
+ {
+ get => Enum.Parse<SessionType>(this[SESSION_TYPE_ENTRY]);
+ protected set => this[SESSION_TYPE_ENTRY] = ((byte)value).ToString();
+ }
+
+ ///<inheritdoc/>
+ public virtual ulong Privilages
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => Convert.ToUInt64(this[PRIV_ENTRY], 16);
+ //Store in hexadecimal to conserve space
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set => this[PRIV_ENTRY] = value.ToString("X");
+ }
+ ///<inheritdoc/>
+ public bool IsNew
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => Flags.IsSet(IS_NEW_MSK);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set => Flags.Set(IS_NEW_MSK, value);
+ }
+ ///<inheritdoc/>
+ public virtual string UserID
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this[USER_ID_ENTRY];
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set => this[USER_ID_ENTRY] = value;
+ }
+ ///<inheritdoc/>
+ public virtual string Token
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this[TOKEN_ENTRY];
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set => this[TOKEN_ENTRY] = value;
+ }
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public virtual void Invalidate(bool all = false)
+ {
+ Flags.Set(INVALID_MSK);
+ Flags.Set(ALL_INVALID_MSK, all);
+ }
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public virtual void RegenID() => Flags.Set(REGEN_ID_MSK);
+ /// <summary>
+ /// Invoked when the indexer is is called to
+ /// </summary>
+ /// <param name="key">The key/index to get the value for</param>
+ /// <returns>The value stored at the specified key</returns>
+ protected abstract string IndexerGet(string key);
+ /// <summary>
+ /// Sets a value requested by the indexer
+ /// </summary>
+ /// <param name="key">The key to associate the value with</param>
+ /// <param name="value">The value to store</param>
+ protected abstract void IndexerSet(string key, string value);
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Sessions/SessionCacheLimitException.cs b/Plugins.Essentials/src/Sessions/SessionCacheLimitException.cs
new file mode 100644
index 0000000..ffa4d9a
--- /dev/null
+++ b/Plugins.Essentials/src/Sessions/SessionCacheLimitException.cs
@@ -0,0 +1,41 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: SessionCacheLimitException.cs
+*
+* SessionCacheLimitException.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;
+
+namespace VNLib.Plugins.Essentials.Sessions
+{
+ /// <summary>
+ /// Raised when the maximum number of cache entires has been reached, and the new session cannot be processed
+ /// </summary>
+ internal class SessionCacheLimitException : SessionException
+ {
+ public SessionCacheLimitException(string message) : base(message)
+ {}
+ public SessionCacheLimitException(string message, Exception innerException) : base(message, innerException)
+ {}
+ public SessionCacheLimitException()
+ {}
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Sessions/SessionException.cs b/Plugins.Essentials/src/Sessions/SessionException.cs
new file mode 100644
index 0000000..554c55f
--- /dev/null
+++ b/Plugins.Essentials/src/Sessions/SessionException.cs
@@ -0,0 +1,48 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: SessionException.cs
+*
+* SessionException.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 System.Runtime.Serialization;
+
+namespace VNLib.Plugins.Essentials.Sessions
+{
+ /// <summary>
+ /// A base class for all session exceptions
+ /// </summary>
+ public class SessionException : Exception
+ {
+ ///<inheritdoc/>
+ public SessionException()
+ {}
+ ///<inheritdoc/>
+ public SessionException(string message) : base(message)
+ {}
+ ///<inheritdoc/>
+ public SessionException(string message, Exception innerException) : base(message, innerException)
+ {}
+ ///<inheritdoc/>
+ protected SessionException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {}
+ }
+}
diff --git a/Plugins.Essentials/src/Sessions/SessionHandle.cs b/Plugins.Essentials/src/Sessions/SessionHandle.cs
new file mode 100644
index 0000000..15c2743
--- /dev/null
+++ b/Plugins.Essentials/src/Sessions/SessionHandle.cs
@@ -0,0 +1,123 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: SessionHandle.cs
+*
+* SessionHandle.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 System.Threading.Tasks;
+using System.Diagnostics.CodeAnalysis;
+
+using VNLib.Net.Http;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Sessions
+{
+ public delegate ValueTask SessionReleaseCallback(ISession session, IHttpEvent @event);
+
+ /// <summary>
+ /// A handle that holds exclusive access to a <see cref="ISession"/>
+ /// session object
+ /// </summary>
+ public readonly struct SessionHandle : IEquatable<SessionHandle>
+ {
+ /// <summary>
+ /// An empty <see cref="SessionHandle"/> instance. (A handle without a session object)
+ /// </summary>
+ public static readonly SessionHandle Empty = new(null, FileProcessArgs.Continue, null);
+
+ private readonly SessionReleaseCallback? ReleaseCb;
+
+ internal readonly bool IsSet => SessionData != null;
+
+ /// <summary>
+ /// The session data object associated with the current session
+ /// </summary>
+ public readonly ISession? SessionData { get; }
+
+ /// <summary>
+ /// A value indicating if the connection is valid and should continue to be processed
+ /// </summary>
+ public readonly FileProcessArgs EntityStatus { get; }
+
+ /// <summary>
+ /// Initializes a new <see cref="SessionHandle"/>
+ /// </summary>
+ /// <param name="sessionData">The session data instance</param>
+ /// <param name="callback">A callback that is invoked when the handle is released</param>
+ /// <param name="entityStatus"></param>
+ public SessionHandle(ISession? sessionData, FileProcessArgs entityStatus, SessionReleaseCallback? callback)
+ {
+ SessionData = sessionData;
+ ReleaseCb = callback;
+ EntityStatus = entityStatus;
+ }
+ /// <summary>
+ /// Initializes a new <see cref="SessionHandle"/>
+ /// </summary>
+ /// <param name="sessionData">The session data instance</param>
+ /// <param name="callback">A callback that is invoked when the handle is released</param>
+ public SessionHandle(ISession sessionData, SessionReleaseCallback callback):this(sessionData, FileProcessArgs.Continue, callback)
+ {}
+
+ /// <summary>
+ /// Releases the session from use
+ /// </summary>
+ /// <param name="event">The current connection event object</param>
+ public ValueTask ReleaseAsync(IHttpEvent @event) => ReleaseCb?.Invoke(SessionData!, @event) ?? ValueTask.CompletedTask;
+
+ /// <summary>
+ /// Determines if another <see cref="SessionHandle"/> is equal to the current handle.
+ /// Handles are equal if neither handle is set or if their SessionData object is equal.
+ /// </summary>
+ /// <param name="other">The other handle to</param>
+ /// <returns>true if neither handle is set or if their SessionData object is equal, false otherwise</returns>
+ public bool Equals(SessionHandle other)
+ {
+ //If neither handle is set, then they are equal, otherwise they are equal if the session objects themselves are equal
+ return (!IsSet && !other.IsSet) || (SessionData?.Equals(other.SessionData) ?? false);
+ }
+ ///<inheritdoc/>
+ public override bool Equals([NotNullWhen(true)] object? obj) => (obj is SessionHandle other) && Equals(other);
+ ///<inheritdoc/>
+ public override int GetHashCode()
+ {
+ return IsSet ? SessionData!.GetHashCode() : base.GetHashCode();
+ }
+
+ /// <summary>
+ /// Checks if two <see cref="SessionHandle"/> instances are equal
+ /// </summary>
+ /// <param name="left"></param>
+ /// <param name="right"></param>
+ /// <returns></returns>
+ public static bool operator ==(SessionHandle left, SessionHandle right) => left.Equals(right);
+
+ /// <summary>
+ /// Checks if two <see cref="SessionHandle"/> instances are not equal
+ /// </summary>
+ /// <param name="left"></param>
+ /// <param name="right"></param>
+ /// <returns></returns>
+ public static bool operator !=(SessionHandle left, SessionHandle right) => !(left == right);
+ }
+}
diff --git a/Plugins.Essentials/src/Sessions/SessionInfo.cs b/Plugins.Essentials/src/Sessions/SessionInfo.cs
new file mode 100644
index 0000000..13e2a84
--- /dev/null
+++ b/Plugins.Essentials/src/Sessions/SessionInfo.cs
@@ -0,0 +1,231 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: SessionInfo.cs
+*
+* SessionInfo.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 System.Net;
+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
+ * the HttpEntity object, so have a single larger object
+ * passed by ref, and created once per request. It may even
+ * be cached and reused in the future. But for now user-apis
+ * should not be cached until a safe use policy is created.
+ */
+
+#pragma warning disable CA1051 // Do not declare visible instance fields
+
+namespace VNLib.Plugins.Essentials.Sessions
+{
+ /// <summary>
+ /// When attached to a connection, provides persistant session storage and inforamtion based
+ /// on a connection.
+ /// </summary>
+ public readonly struct SessionInfo : IObjectStorage, IEquatable<SessionInfo>
+ {
+ /// <summary>
+ /// A value indicating if the current instance has been initiailzed
+ /// with a session. Otherwise properties are undefied
+ /// </summary>
+ public readonly bool IsSet;
+
+ private readonly ISession UserSession;
+ /// <summary>
+ /// Key that identifies the current session. (Identical to cookie::sessionid)
+ /// </summary>
+ public readonly string 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;
+ /// <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/>
+ public T GetObject<T>(string key)
+ {
+ //Attempt to deserialze the object, or return default if it is empty
+ return this[key].AsJsonObject<T>(SR_OPTIONS);
+ }
+ ///<inheritdoc/>
+ public void SetObject<T>(string key, T obj)
+ {
+ //Serialize and store the object, or set null (remove) if the object is null
+ this[key] = obj?.ToJsonString(SR_OPTIONS);
+ }
+
+ /// <summary>
+ /// Was the original session cross origin?
+ /// </summary>
+ public readonly bool CrossOrigin;
+ /// <summary>
+ /// The origin header specified during session creation
+ /// </summary>
+ public readonly Uri SpecifiedOrigin;
+ /// <summary>
+ /// Privilages associated with user specified during login
+ /// </summary>
+ public readonly DateTimeOffset Created;
+ /// <summary>
+ /// Was this session just created on this connection?
+ /// </summary>
+ public readonly bool IsNew;
+ /// <summary>
+ /// Gets or sets the session's login hash, if set to a non-empty/null value, will trigger an upgrade on close
+ /// </summary>
+ public readonly string LoginHash
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => UserSession.GetLoginToken();
+ [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>
+ public readonly string Token
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => UserSession.Token;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set => UserSession.Token = value;
+ }
+ /// <summary>
+ /// <para>
+ /// Gets or sets the user-id for the current session.
+ /// </para>
+ /// <para>
+ /// Login code usually sets this value and it should be read-only
+ /// </para>
+ /// </summary>
+ public readonly string UserID
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => UserSession.UserID;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set => UserSession.UserID = value;
+ }
+ /// <summary>
+ /// Privilages associated with user specified during login
+ /// </summary>
+ public readonly ulong Privilages
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => UserSession.Privilages;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set => UserSession.Privilages = value;
+ }
+ /// <summary>
+ /// The IP address belonging to the client
+ /// </summary>
+ public readonly IPAddress 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>
+ public readonly SessionType SessionType => UserSession.SessionType;
+
+ /// <summary>
+ /// Accesses the session's general storage
+ /// </summary>
+ /// <param name="index">Key for specifie data</param>
+ /// <returns>Value associated with the key from the session's general storage</returns>
+ public readonly string this[string index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => UserSession[index];
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set => UserSession[index] = value;
+ }
+
+ internal SessionInfo(ISession session, IConnectionInfo ci, IPAddress trueIp)
+ {
+ UserSession = session;
+ //Calculate and store
+ IsNew = session.IsNew;
+ SessionID = session.SessionID;
+ Created = session.Created;
+ UserIP = session.UserIP;
+ //Ip match
+ IPMatch = trueIp.Equals(session.UserIP);
+ //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;
+ }
+ else
+ {
+ //Load/decode stored variables
+ UserAgent = session.GetUserAgent();
+ SpecifiedOrigin = session.GetOriginUri();
+ CrossOrigin = session.IsCrossOrigin();
+ SecurityProcol = session.GetSecurityProtocol();
+ }
+ CrossOriginMatch = ci.Origin != null && ci.Origin.Equals(SpecifiedOrigin);
+ IsSet = true;
+ }
+
+ ///<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);
+ ///<inheritdoc/>
+ public override int GetHashCode() => SessionID.GetHashCode(StringComparison.Ordinal);
+ ///<inheritdoc/>
+ public static bool operator ==(SessionInfo left, SessionInfo right) => left.Equals(right);
+ ///<inheritdoc/>
+ public static bool operator !=(SessionInfo left, SessionInfo right) => !(left == right);
+
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Statics.cs b/Plugins.Essentials/src/Statics.cs
new file mode 100644
index 0000000..58b5dd7
--- /dev/null
+++ b/Plugins.Essentials/src/Statics.cs
@@ -0,0 +1,44 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: Statics.cs
+*
+* Statics.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 System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace VNLib.Plugins.Essentials
+{
+ public static class Statics
+ {
+ public static readonly JsonSerializerOptions SR_OPTIONS = new()
+ {
+ DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
+ NumberHandling = JsonNumberHandling.Strict,
+ ReadCommentHandling = JsonCommentHandling.Disallow,
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ IgnoreReadOnlyFields = true,
+ DefaultBufferSize = Environment.SystemPageSize,
+ };
+ }
+}
diff --git a/Plugins.Essentials/src/TimestampedCounter.cs b/Plugins.Essentials/src/TimestampedCounter.cs
new file mode 100644
index 0000000..19cb8ec
--- /dev/null
+++ b/Plugins.Essentials/src/TimestampedCounter.cs
@@ -0,0 +1,117 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: TimestampedCounter.cs
+*
+* TimestampedCounter.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
+{
+ /// <summary>
+ /// Stucture that allows for convient storage of a counter value
+ /// and a second precision timestamp into a 64-bit unsigned integer
+ /// </summary>
+ public readonly struct TimestampedCounter : IEquatable<TimestampedCounter>
+ {
+ /// <summary>
+ /// The time the count was last modifed
+ /// </summary>
+ public readonly DateTimeOffset LastModified;
+ /// <summary>
+ /// The last failed login attempt count value
+ /// </summary>
+ public readonly uint Count;
+
+ /// <summary>
+ /// Initalizes a new flc structure with the current UTC date
+ /// and the specified count value
+ /// </summary>
+ /// <param name="count">FLC current count</param>
+ public TimestampedCounter(uint count) : this(DateTimeOffset.UtcNow, count)
+ { }
+
+ private TimestampedCounter(DateTimeOffset dto, uint count)
+ {
+ Count = count;
+ LastModified = dto;
+ }
+
+ /// <summary>
+ /// Compacts and converts the counter value and timestamp into
+ /// a 64bit unsigned integer
+ /// </summary>
+ /// <param name="count">The counter to convert</param>
+ public static explicit operator ulong(TimestampedCounter count) => count.ToUInt64();
+
+ /// <summary>
+ /// Compacts and converts the counter value and timestamp into
+ /// a 64bit unsigned integer
+ /// </summary>
+ /// <returns>The uint64 compacted value</returns>
+ public ulong ToUInt64()
+ {
+ //Upper 32 bits time, lower 32 bits count
+ ulong value = (ulong)LastModified.ToUnixTimeSeconds() << 32;
+ value |= Count;
+ return value;
+ }
+
+ /// <summary>
+ /// The previously compacted <see cref="TimestampedCounter"/>
+ /// value to cast back to a counter
+ /// </summary>
+ /// <param name="value"></param>
+ public static explicit operator TimestampedCounter(ulong value) => FromUInt64(value);
+
+ ///<inheritdoc/>
+ public override bool Equals(object? obj) => obj is TimestampedCounter counter && Equals(counter);
+ ///<inheritdoc/>
+ public override int GetHashCode() => this.ToUInt64().GetHashCode();
+ ///<inheritdoc/>
+ public static bool operator ==(TimestampedCounter left, TimestampedCounter right) => left.Equals(right);
+ ///<inheritdoc/>
+ public static bool operator !=(TimestampedCounter left, TimestampedCounter right) => !(left == right);
+ ///<inheritdoc/>
+ public bool Equals(TimestampedCounter other) => ToUInt64() == other.ToUInt64();
+
+ /// <summary>
+ /// The previously compacted <see cref="TimestampedCounter"/>
+ /// value to cast back to a counter
+ /// </summary>
+ /// <param name="value">The uint64 encoded <see cref="TimestampedCounter"/></param>
+ /// <returns>
+ /// The decoded <see cref="TimestampedCounter"/> from its
+ /// compatcted representation
+ /// </returns>
+ public static TimestampedCounter FromUInt64(ulong value)
+ {
+ //Upper 32 bits time, lower 32 bits count
+ long time = (long)(value >> 32);
+ uint count = (uint)(value & uint.MaxValue);
+ //Init dto struct
+ return new(DateTimeOffset.FromUnixTimeSeconds(time), count);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Users/IUser.cs b/Plugins.Essentials/src/Users/IUser.cs
new file mode 100644
index 0000000..28c5305
--- /dev/null
+++ b/Plugins.Essentials/src/Users/IUser.cs
@@ -0,0 +1,74 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: IUser.cs
+*
+* IUser.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 System.Collections.Generic;
+
+using VNLib.Utils;
+using VNLib.Utils.Async;
+using VNLib.Utils.Memory;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Users
+{
+ /// <summary>
+ /// Represents an abstract user account
+ /// </summary>
+ public interface IUser : IAsyncExclusiveResource, IDisposable, IObjectStorage, IEnumerable<KeyValuePair<string, string>>, IIndexable<string, string>
+ {
+ /// <summary>
+ /// The user's privilage level
+ /// </summary>
+ ulong Privilages { get; }
+ /// <summary>
+ /// The user's ID
+ /// </summary>
+ string UserID { get; }
+ /// <summary>
+ /// Date the user's account was created
+ /// </summary>
+ DateTimeOffset Created { get; }
+ /// <summary>
+ /// The user's password hash if retreived from the backing store, otherwise null
+ /// </summary>
+ PrivateString? PassHash { get; }
+ /// <summary>
+ /// Status of account
+ /// </summary>
+ UserStatus Status { get; set; }
+ /// <summary>
+ /// Is the account only usable from local network?
+ /// </summary>
+ bool LocalOnly { get; set; }
+ /// <summary>
+ /// The user's email address
+ /// </summary>
+ string EmailAddress { get; set; }
+ /// <summary>
+ /// Marks the user for deletion on release
+ /// </summary>
+ void Delete();
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Users/IUserManager.cs b/Plugins.Essentials/src/Users/IUserManager.cs
new file mode 100644
index 0000000..dd521e4
--- /dev/null
+++ b/Plugins.Essentials/src/Users/IUserManager.cs
@@ -0,0 +1,103 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: IUserManager.cs
+*
+* IUserManager.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 System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Users
+{
+ /// <summary>
+ /// A backing store that provides user accounts
+ /// </summary>
+ public interface IUserManager
+ {
+ /// <summary>
+ /// Attempts to get a user object without their password from the database asynchronously
+ /// </summary>
+ /// <param name="userId">The id of the user</param>
+ /// <param name="cancellationToken">A token to cancel the operation</param>
+ /// <returns>The user's <see cref="IUser"/> object, null if the user was not found</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ Task<IUser?> GetUserFromIDAsync(string userId, CancellationToken cancellationToken = default);
+ /// <summary>
+ /// Attempts to get a user object without their password from the database asynchronously
+ /// </summary>
+ /// <param name="emailAddress">The user's email address</param>
+ /// <param name="cancellationToken">A token to cancel the operation</param>
+ /// <returns>The user's <see cref="IUser"/> object, null if the user was not found</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ Task<IUser?> GetUserFromEmailAsync(string emailAddress, CancellationToken cancellationToken = default);
+ /// <summary>
+ /// Attempts to get a user object with their password from the database on the current thread
+ /// </summary>
+ /// <param name="userid">The id of the user</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>The user's <see cref="IUser"/> object, null if the user was not found</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ Task<IUser?> GetUserAndPassFromIDAsync(string userid, CancellationToken cancellation = default);
+ /// <summary>
+ /// Attempts to get a user object with their password from the database asynchronously
+ /// </summary>
+ /// <param name="emailAddress">The user's email address</param>
+ /// <param name="cancellationToken">A token to cancel the operation</param>
+ /// <returns>The user's <see cref="IUser"/> object, null if the user was not found</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ Task<IUser?> GetUserAndPassFromEmailAsync(string emailAddress, CancellationToken cancellationToken = default);
+ /// <summary>
+ /// Creates a new user in the current user's table and if successful returns the new user object (without password)
+ /// </summary>
+ /// <param name="userid">The user id</param>
+ /// <param name="privilages">A number representing the privilage level of the account</param>
+ /// <param name="passHash">Value to store in the password field</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <param name="emailAddress">The account email address</param>
+ /// <returns>An object representing a user's account if successful, null otherwise</returns>
+ /// <exception cref="UserExistsException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="UserCreationFailedException"></exception>
+ Task<IUser> CreateUserAsync(string userid, string emailAddress, ulong privilages, PrivateString passHash, CancellationToken cancellation = default);
+ /// <summary>
+ /// Updates a password associated with the specified user. If the update fails, the transaction
+ /// is rolled back.
+ /// </summary>
+ /// <param name="user">The user account to update the password of</param>
+ /// <param name="newPass">The new password to set</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>The result of the operation, the result should be 1 (aka true)</returns>
+ Task<ERRNO> UpdatePassAsync(IUser user, PrivateString newPass, CancellationToken cancellation = default);
+
+ /// <summary>
+ /// Gets the number of entries in the current user table
+ /// </summary>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>The number of users in the table, or -1 if the operation failed</returns>
+ Task<long> GetUserCountAsync(CancellationToken cancellation = default);
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Users/UserCreationFailedException.cs b/Plugins.Essentials/src/Users/UserCreationFailedException.cs
new file mode 100644
index 0000000..9f509ac
--- /dev/null
+++ b/Plugins.Essentials/src/Users/UserCreationFailedException.cs
@@ -0,0 +1,47 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: UserCreationFailedException.cs
+*
+* UserCreationFailedException.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 System.Runtime.Serialization;
+using VNLib.Utils.Resources;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Users
+{
+ /// <summary>
+ /// Raised when a user creation operation has failed and could not be created
+ /// </summary>
+ public class UserCreationFailedException : ResourceUpdateFailedException
+ {
+ public UserCreationFailedException()
+ {}
+ public UserCreationFailedException(string message) : base(message)
+ {}
+ public UserCreationFailedException(string message, Exception innerException) : base(message, innerException)
+ {}
+ protected UserCreationFailedException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {}
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Users/UserDeleteException.cs b/Plugins.Essentials/src/Users/UserDeleteException.cs
new file mode 100644
index 0000000..cd26543
--- /dev/null
+++ b/Plugins.Essentials/src/Users/UserDeleteException.cs
@@ -0,0 +1,44 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: UserDeleteException.cs
+*
+* UserDeleteException.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.Resources;
+
+namespace VNLib.Plugins.Essentials.Users
+{
+ /// <summary>
+ /// Raised when a user flagged for deletion could not be deleted. See the <see cref="Exception.InnerException"/>
+ /// for the Exception that cause the opertion to fail
+ /// </summary>
+ public class UserDeleteException : ResourceDeleteFailedException
+ {
+ public UserDeleteException(string message, Exception cause) : base(message, cause) { }
+
+ public UserDeleteException()
+ {}
+
+ public UserDeleteException(string message) : base(message)
+ {}
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Users/UserExistsException.cs b/Plugins.Essentials/src/Users/UserExistsException.cs
new file mode 100644
index 0000000..5c63547
--- /dev/null
+++ b/Plugins.Essentials/src/Users/UserExistsException.cs
@@ -0,0 +1,49 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: UserExistsException.cs
+*
+* UserExistsException.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 System.Runtime.Serialization;
+
+namespace VNLib.Plugins.Essentials.Users
+{
+ /// <summary>
+ /// Raised when an <see cref="IUserManager"/> operation
+ /// fails because the user account already exists
+ /// </summary>
+ public class UserExistsException : UserCreationFailedException
+ {
+ ///<inheritdoc/>
+ public UserExistsException()
+ {}
+ ///<inheritdoc/>
+ public UserExistsException(string message) : base(message)
+ {}
+ ///<inheritdoc/>
+ public UserExistsException(string message, Exception innerException) : base(message, innerException)
+ {}
+ ///<inheritdoc/>
+ protected UserExistsException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {}
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Users/UserStatus.cs b/Plugins.Essentials/src/Users/UserStatus.cs
new file mode 100644
index 0000000..32aa63d
--- /dev/null
+++ b/Plugins.Essentials/src/Users/UserStatus.cs
@@ -0,0 +1,50 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: UserStatus.cs
+*
+* UserStatus.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/.
+*/
+
+namespace VNLib.Plugins.Essentials.Users
+{
+ public enum UserStatus
+ {
+ /// <summary>
+ /// Unverified account state
+ /// </summary>
+ Unverified,
+ /// <summary>
+ /// Active account state. The account is fully functional
+ /// </summary>
+ Active,
+ /// <summary>
+ /// The account is suspended
+ /// </summary>
+ Suspended,
+ /// <summary>
+ /// The account is inactive as marked by the system
+ /// </summary>
+ Inactive,
+ /// <summary>
+ /// The account has been locked from access
+ /// </summary>
+ Locked
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Users/UserUpdateException.cs b/Plugins.Essentials/src/Users/UserUpdateException.cs
new file mode 100644
index 0000000..391bb05
--- /dev/null
+++ b/Plugins.Essentials/src/Users/UserUpdateException.cs
@@ -0,0 +1,43 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: UserUpdateException.cs
+*
+* UserUpdateException.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.Resources;
+
+namespace VNLib.Plugins.Essentials.Users
+{
+ /// <summary>
+ /// Raised when a user-data object was modified and an update operation failed
+ /// </summary>
+ public class UserUpdateException : ResourceUpdateFailedException
+ {
+ public UserUpdateException(string message, Exception cause) : base(message, cause) { }
+
+ public UserUpdateException()
+ {}
+
+ public UserUpdateException(string message) : base(message)
+ {}
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj b/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj
new file mode 100644
index 0000000..b1751bd
--- /dev/null
+++ b/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj
@@ -0,0 +1,53 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <RootNamespace>VNLib.Plugins.Essentials</RootNamespace>
+ <Company>$(Authors)</Company>
+ <Authors>Vaughn Nugent</Authors>
+ <Product>VNLib Essentials Plugin Library</Product>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <AssemblyVersion></AssemblyVersion>
+ <FileVersion></FileVersion>
+ <Description>Provides essential web, user, storage, and database interaction features for use with web applications</Description>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <AssemblyName>VNLib.Plugins.Essentials</AssemblyName>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+
+ <!-- Resolve nuget dll files and store them in the output dir -->
+ <PropertyGroup>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ <PackageTags>VNLib, Plugins, VNLib.Plugins.Essentials, Essentials, Essential Plugins, HTTP Essentials, OAuth2</PackageTags>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <Version>1.0.1.3</Version>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ <EnableNETAnalyzers>True</EnableNETAnalyzers>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\Hashing\src\VNLib.Hashing.Portable.csproj" />
+ <ProjectReference Include="..\..\Http\src\VNLib.Net.Http.csproj" />
+ <ProjectReference Include="..\..\Plugins\src\VNLib.Plugins.csproj" />
+ <ProjectReference Include="..\..\Utils\src\VNLib.Utils.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins.Essentials/src/WebSocketSession.cs b/Plugins.Essentials/src/WebSocketSession.cs
new file mode 100644
index 0000000..106501c
--- /dev/null
+++ b/Plugins.Essentials/src/WebSocketSession.cs
@@ -0,0 +1,204 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: WebSocketSession.cs
+*
+* WebSocketSession.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 System.IO;
+using System.Threading;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+
+using VNLib.Net.Http;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials
+{
+ /// <summary>
+ /// A callback method to invoke when an HTTP service successfully transfers protocols to
+ /// the WebSocket protocol and the socket is ready to be used
+ /// </summary>
+ /// <param name="session">The open websocket session instance</param>
+ /// <returns>
+ /// A <see cref="Task"/> that will be awaited by the HTTP layer. When the task completes, the transport
+ /// will be closed and the session disposed
+ /// </returns>
+
+ public delegate Task WebsocketAcceptedCallback(WebSocketSession session);
+
+ /// <summary>
+ /// Represents a <see cref="WebSocket"/> wrapper to manage the lifetime of the captured
+ /// connection context and the underlying transport. This session is managed by the parent
+ /// <see cref="HttpServer"/> that it was created on.
+ /// </summary>
+ public sealed class WebSocketSession : AlternateProtocolBase
+ {
+ private WebSocket? WsHandle;
+ private readonly WebsocketAcceptedCallback AcceptedCallback;
+
+ /// <summary>
+ /// A cancellation token that can be monitored to reflect the state
+ /// of the webscocket
+ /// </summary>
+ public CancellationToken Token => CancelSource.Token;
+
+ /// <summary>
+ /// Id assigned to this instance on creation
+ /// </summary>
+ public string SocketID { get; }
+
+ /// <summary>
+ /// Negotiated sub-protocol
+ /// </summary>
+ public string? SubProtocol { get; }
+
+ /// <summary>
+ /// A user-defined state object passed during socket accept handshake
+ /// </summary>
+ public object? UserState { get; internal set; }
+
+ internal WebSocketSession(string? subProtocol, WebsocketAcceptedCallback callback)
+ : this(Guid.NewGuid().ToString("N"), subProtocol, callback)
+ { }
+
+ internal WebSocketSession(string socketId, string? subProtocol, WebsocketAcceptedCallback callback)
+ {
+ SocketID = socketId;
+ SubProtocol = subProtocol;
+ //Store the callback function
+ AcceptedCallback = callback;
+ }
+
+ /// <summary>
+ /// Initialzes the created websocket with the specified protocol
+ /// </summary>
+ /// <param name="transport">Transport stream to use for the websocket</param>
+ /// <returns>The accept callback function specified during object initialization</returns>
+ protected override async Task RunAsync(Stream transport)
+ {
+ try
+ {
+ WebSocketCreationOptions ce = new()
+ {
+ IsServer = true,
+ KeepAliveInterval = TimeSpan.FromSeconds(30),
+ SubProtocol = SubProtocol,
+ };
+
+ //Create a new websocket from the context stream
+ WsHandle = WebSocket.CreateFromStream(transport, ce);
+
+ //Register token to abort the websocket so the managed ws uses the non-fallback send/recv method
+ using CancellationTokenRegistration abortReg = Token.Register(WsHandle.Abort);
+
+ //Return the callback function to explcitly invoke it
+ await AcceptedCallback(this);
+ }
+ finally
+ {
+ WsHandle?.Dispose();
+ UserState = null;
+ }
+ }
+
+ /// <summary>
+ /// Asynchronously receives data from the Websocket and copies the data to the specified buffer
+ /// </summary>
+ /// <param name="buffer">The buffer to store read data</param>
+ /// <returns>A task that resolves a <see cref="WebSocketReceiveResult"/> which contains the status of the operation</returns>
+ /// <exception cref="OperationCanceledException"></exception>
+ public Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer)
+ {
+ //Begin receive operation only with the internal token
+ return WsHandle!.ReceiveAsync(buffer, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Asynchronously receives data from the Websocket and copies the data to the specified buffer
+ /// </summary>
+ /// <param name="buffer">The buffer to store read data</param>
+ /// <returns></returns>
+ /// <exception cref="OperationCanceledException"></exception>
+ public ValueTask<ValueWebSocketReceiveResult> ReceiveAsync(Memory<byte> buffer)
+ {
+ //Begin receive operation only with the internal token
+ return WsHandle!.ReceiveAsync(buffer, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Asynchronously sends the specified buffer to the client of the specified type
+ /// </summary>
+ /// <param name="buffer">The buffer containing data to send</param>
+ /// <param name="type">The message/data type of the packet to send</param>
+ /// <param name="endOfMessage">A value that indicates this message is the final message of the transaction</param>
+ /// <returns></returns>
+ /// <exception cref="OperationCanceledException"></exception>
+ public Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType type, bool endOfMessage)
+ {
+ //Create a send request with
+ return WsHandle!.SendAsync(buffer, type, endOfMessage, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Asynchronously sends the specified buffer to the client of the specified type
+ /// </summary>
+ /// <param name="buffer">The buffer containing data to send</param>
+ /// <param name="type">The message/data type of the packet to send</param>
+ /// <param name="endOfMessage">A value that indicates this message is the final message of the transaction</param>
+ /// <returns></returns>
+ /// <exception cref="OperationCanceledException"></exception>
+ public ValueTask SendAsync(ReadOnlyMemory<byte> buffer, WebSocketMessageType type, bool endOfMessage)
+ {
+ //Begin receive operation only with the internal token
+ return WsHandle!.SendAsync(buffer, type, endOfMessage, CancellationToken.None);
+ }
+
+
+ /// <summary>
+ /// Properly closes a currently connected websocket
+ /// </summary>
+ /// <param name="status">Set the close status</param>
+ /// <param name="reason">Set the close reason</param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public Task CloseSocketAsync(WebSocketCloseStatus status, string reason)
+ {
+ return WsHandle!.CloseAsync(status, reason, CancellationToken.None);
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="status"></param>
+ /// <param name="reason"></param>
+ /// <param name="cancellation"></param>
+ /// <returns></returns>
+ public Task CloseSocketOutputAsync(WebSocketCloseStatus status, string reason, CancellationToken cancellation = default)
+ {
+ if (WsHandle!.State == WebSocketState.Open || WsHandle.State == WebSocketState.CloseSent)
+ {
+ return WsHandle.CloseOutputAsync(status, reason, cancellation);
+ }
+ return Task.CompletedTask;
+ }
+ }
+} \ No newline at end of file