aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-08-19 23:44:53 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-08-19 23:44:53 -0400
commitf541b853c738a076d2a85ef6b269c1b140353b85 (patch)
treeae52056036d22963613903181029c3ff9bed0f5a
parent3f5eb61fc7166674a5424d5f8e8c23a775c27614 (diff)
Essentials middleware first addition, websocket feature and generics
-rw-r--r--README.md6
-rw-r--r--lib/Plugins.Essentials/src/EventProcessor.cs206
-rw-r--r--lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs98
-rw-r--r--lib/Plugins.Essentials/src/IVirtualEndpointTable.cs73
-rw-r--r--lib/Plugins.Essentials/src/Middleware/HttpMiddlewareResult.cs42
-rw-r--r--lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs44
-rw-r--r--lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.cs67
-rw-r--r--lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs86
-rw-r--r--lib/Plugins.Essentials/src/SemiConsistentVeTable.cs175
-rw-r--r--lib/Plugins.Essentials/src/WebSocketSession.cs83
10 files changed, 683 insertions, 197 deletions
diff --git a/README.md b/README.md
index bdbd3ac..ea81542 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
*A Mono-repo for "core" VNLib projects for simplicity.*
### What is VNLib.Core?
-VNLib is a collection of .NET/C# (mostly) libraries that I maintain for many private and public projects. VNLib.Core is a subset of the larger collection of projects I have named VNLib. This repo contains libraries I consider to be utility only, or building blocks that are individually useful for other projects. You will likely see many or most of them used across may other VNLib type projects. These libraries are meant to be stand-alone, meaning that there are no required* external dependencies (except the [mscorelib](https://github.com/dotnet/runtime)). For example the [VNLib.Utils](lib/Utils/#) library is a standalone, 0 required dependency library that is useful for, logging, common extensions, and a significant collection of memory related utilities.
+VNLib is a collection of cross platform .NET/C#/C libraries that I maintain for many private and public projects. VNLib.Core is a subset of the larger collection of projects I have named VNLib. This repo contains libraries I consider to be utility only, or building blocks that are individually useful for other projects. You will likely see many or most of them used across may other VNLib type projects. These libraries are meant to be stand-alone, meaning that there are no required* external dependencies (except the [mscorelib](https://github.com/dotnet/runtime)). For example the [VNLib.Utils](lib/Utils/#) library is a standalone, 0 required dependency library that is useful for, logging, common extensions, and a significant collection of memory related utilities.
### Dependencies
Any libraries in this repository that contain external dependencies will be mentioned explicitly in the library's readme. I intend to limit this behavior, as it is the reason this repository exists.
@@ -18,7 +18,7 @@ Docs and articles will be available from the docs link below . There are docs pe
Again, go to my website below, my email address is available, go ahead and send me a message. Or use the email address from my profile to send me an email (via proton mail for now)
### Links
-[Home Page](https://www.vaughnnugent.com)- Website home page
+[Home Page](https://www.vaughnnugent.com) - Website home page
[Documentation](https://www.vaughnnugent.com/resources/software/articles?tags=docs,_VNLib.Core) - Docs and articles for this module
[Builds for VNLib.Core](https://www.vaughnnugent.com/resources/software/modules/VNLib.Core) - Per-project build artifacts
[Links for Nuget Feeds](https://www.vaughnnugent.com/resources/software/modules) - Get my NuGet feed links
@@ -37,7 +37,7 @@ Again, go to my website below, my email address is available, go ahead and send
- [Plugins.PluginBase](lib/Plugins.PluginBase/#) - Base library/api for plugin developers to build fully managed/supported runtime loaded plugins, without worrying about the plumbing, such as the IPlugin api, endpoint creation, and logging! This library is required if you wish to use most of the Plugin.Extensions libraries.
- [Net.Messaging.FBM](lib/Net.Messaging.FBM/#) - Fixed Buffer Messaging protocol, high performance, request/response architecture, client & server library, built atop http and web-sockets. As implied, relies on fixed sized internal buffers that are negotiated to transfer data with minimal overhead for known messaging architectures.
- [WinRpMalloc](lib/WinRpMalloc/#) - A Windows x64 dll project that exposes the rpmalloc memory allocator as a NativeHeap for .NET Utils library loading in the unmanned heap architecture.
-- [Net.Compression](lib/Net.Compression/#) - A cross platform IHttpCompressorManager configured for runtime dynamic loading for high performance native response data compression.
+- [Net.Compression](lib/Net.Compression/#) - A cross platform native compression provider and IHttpCompressorManager configured for runtime dynamic loading for high performance native response data compression.
- [Net.Rest.Client](lib/Net.Rest.Client/#) - A minimal library that provides a RestSharp client resource pool for concurrent usage with async support, along with an OAuth2 client credentials IAuthenticator implementation for use with Oauth2 plugins.
## Builds & Source
diff --git a/lib/Plugins.Essentials/src/EventProcessor.cs b/lib/Plugins.Essentials/src/EventProcessor.cs
index bd4383f..90906eb 100644
--- a/lib/Plugins.Essentials/src/EventProcessor.cs
+++ b/lib/Plugins.Essentials/src/EventProcessor.cs
@@ -25,7 +25,6 @@
using System;
using System.IO;
using System.Net;
-using System.Linq;
using System.Threading;
using System.Net.Sockets;
using System.Threading.Tasks;
@@ -39,12 +38,12 @@ using VNLib.Plugins.Essentials.Accounts;
using VNLib.Plugins.Essentials.Content;
using VNLib.Plugins.Essentials.Sessions;
using VNLib.Plugins.Essentials.Extensions;
+using VNLib.Plugins.Essentials.Middleware;
#nullable enable
namespace VNLib.Plugins.Essentials
{
-
/// <summary>
/// Provides an abstract base implementation of <see cref="IWebRoot"/>
@@ -64,6 +63,7 @@ namespace VNLib.Plugins.Essentials
/// The filesystem entrypoint path for the site
/// </summary>
public abstract string Directory { get; }
+
///<inheritdoc/>
public abstract string Hostname { get; }
@@ -92,6 +92,7 @@ namespace VNLib.Plugins.Essentials
/// <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
@@ -104,12 +105,14 @@ namespace VNLib.Plugins.Essentials
/// <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>
@@ -117,14 +120,25 @@ namespace VNLib.Plugins.Essentials
/// <param name="chosenRoutine">The selected file processing routine for the given request</param>
public abstract void PostProcessFile(HttpEntity entity, in FileProcessArgs chosenRoutine);
- #region security
-
///<inheritdoc/>
public abstract IAccountSecurityProvider AccountSecurity { get; }
- #endregion
+ /// <summary>
+ /// The table of virtual endpoints that will be used to process requests
+ /// </summary>
+ /// <remarks>
+ /// May be overriden to provide a custom endpoint table
+ /// </remarks>
+ public virtual IVirtualEndpointTable EndpointTable { get; } = new SemiConsistentVeTable();
+
+ /// <summary>
+ /// The middleware chain that will be used to process requests
+ /// </summary>
+ /// <remarks>
+ /// If derrieved, may be overriden to provide a custom middleware chain
+ /// </remarks>
+ public virtual IHttpMiddlewareChain MiddlewareChain { get; } = new SemiConistentMiddlewareChain();
- #region sessions
/// <summary>
/// An <see cref="ISessionProvider"/> that connects stateful sessions to
@@ -138,10 +152,7 @@ namespace VNLib.Plugins.Essentials
/// </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
@@ -154,156 +165,9 @@ namespace VNLib.Plugins.Essentials
/// </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 sealed record class EvEndpointWrapper(IVirtualEndpoint<IHttpEvent> Wrapped) : IVirtualEndpoint<HttpEntity>
- {
- 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 IReadOnlyDictionary<string, IVirtualEndpoint<HttpEntity>> VirtualEndpoints = new Dictionary<string, IVirtualEndpoint<HttpEntity>>();
-
-
- /*
- * 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 endpoint store
- /// </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
- RemoveEndpoint(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 RemoveEndpoint(params string[] paths)
- {
- _ = paths ?? throw new ArgumentNullException(nameof(paths));
-
- //Make sure all endpoints specify a path
- if (paths.Any(static e => string.IsNullOrWhiteSpace(e)))
- {
- 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
- _ = Interlocked.Exchange(ref VirtualEndpoints, newTable);
- }
- }
-
- #endregion
-
///<inheritdoc/>
public virtual async ValueTask ClientConnectedAsync(IHttpEvent httpEvent)
{
@@ -350,10 +214,32 @@ namespace VNLib.Plugins.Essentials
return;
}
- if (VirtualEndpoints.Count > 0)
+ //Handle middleware before file processing
+ LinkedListNode<IHttpMiddleware>? mwNode = MiddlewareChain.GetCurrentHead();
+
+ //Loop though nodes
+ while(mwNode != null)
+ {
+ //Process
+ HttpMiddlewareResult result = await mwNode.ValueRef.ProcessAsync(entity);
+
+ switch (result)
+ {
+ //move next
+ case HttpMiddlewareResult.Continue:
+ mwNode = mwNode.Next;
+ break;
+
+ //Middleware completed the connection, time to exit
+ case HttpMiddlewareResult.Complete:
+ return;
+ }
+ }
+
+ if (!EndpointTable.IsEmpty)
{
//See if the virtual file is servicable
- if (!VirtualEndpoints.TryGetValue(entity.Server.Path, out IVirtualEndpoint<HttpEntity>? vf))
+ if (!EndpointTable.TryGetEndpoint(entity.Server.Path, out IVirtualEndpoint<HttpEntity>? vf))
{
args = FileProcessArgs.Continue;
}
diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
index bd7f466..17af891 100644
--- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
+++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
@@ -792,18 +792,96 @@ namespace VNLib.Plugins.Essentials.Extensions
[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.
+ /// Attempts 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="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>
+ /// <param name="userState">An object to store in the <see cref="WebSocketSession{T}.UserState"/> property when the websocket has been accepted</param>
+ /// <param name="keepAlive">An optional, explicit web-socket keep-alive interval</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)
+ public static bool AcceptWebSocket<T>(this IHttpEvent entity,
+ WebSocketAcceptedCallback<T> socketOpenedCallback,
+ T userState,
+ string? subProtocol = null,
+ TimeSpan keepAlive = default
+ )
+ {
+ //Must define an accept callback
+ _ = socketOpenedCallback ?? throw new ArgumentNullException(nameof(socketOpenedCallback));
+
+ bool success = PrepWebSocket(entity, subProtocol);
+
+ if (success)
+ {
+ //Set a default keep alive if none was specified
+ if (keepAlive == default)
+ {
+ keepAlive = TimeSpan.FromSeconds(30);
+ }
+
+ IAlternateProtocol ws = new WebSocketSession<T>(GetNewSocketId(), socketOpenedCallback)
+ {
+ SubProtocol = subProtocol,
+ IsSecure = entity.Server.IsSecure(),
+ UserState = userState,
+ KeepAlive = keepAlive,
+ };
+
+ //Setup a new websocket session with a new session id
+ entity.DangerousChangeProtocol(ws);
+ }
+ //Set the client up for a bad request response, nod a valid websocket request
+ entity.CloseResponse(HttpStatusCode.BadRequest);
+ return false;
+ }
+
+ /// <summary>
+ /// Attempts 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="keepAlive">An optional, explicit web-socket keep-alive interval</param>
+ /// <returns>True if operation succeeds.</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ public static bool AcceptWebSocket(this IHttpEvent entity, WebSocketAcceptedCallback socketOpenedCallback, string? subProtocol = null, TimeSpan keepAlive = default)
+ {
+ //Must define an accept callback
+ _ = socketOpenedCallback ?? throw new ArgumentNullException(nameof(socketOpenedCallback));
+
+ bool success = PrepWebSocket(entity, subProtocol);
+
+ if(success)
+ {
+ //Set a default keep alive if none was specified
+ if (keepAlive == default)
+ {
+ keepAlive = TimeSpan.FromSeconds(30);
+ }
+
+ IAlternateProtocol ws = new WebSocketSession(GetNewSocketId(), socketOpenedCallback)
+ {
+ SubProtocol = subProtocol,
+ IsSecure = entity.Server.IsSecure(),
+ KeepAlive = keepAlive,
+ };
+
+ //Setup a new websocket session with a new session id
+ entity.DangerousChangeProtocol(ws);
+ }
+
+ return success;
+ }
+
+ private static string GetNewSocketId() => Guid.NewGuid().ToString("N");
+
+ private static bool PrepWebSocket(this IHttpEvent entity, string? subProtocol = null)
{
//Make sure this is a websocket request
if (!entity.Server.IsWebSocketRequest)
@@ -811,9 +889,6 @@ namespace VNLib.Plugins.Essentials.Extensions
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
@@ -836,13 +911,6 @@ namespace VNLib.Plugins.Essentials.Extensions
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;
}
}
diff --git a/lib/Plugins.Essentials/src/IVirtualEndpointTable.cs b/lib/Plugins.Essentials/src/IVirtualEndpointTable.cs
new file mode 100644
index 0000000..cfe9661
--- /dev/null
+++ b/lib/Plugins.Essentials/src/IVirtualEndpointTable.cs
@@ -0,0 +1,73 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: IVirtualEndpointTable.cs
+*
+* IVirtualEndpointTable.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>
+ /// Represents a table of virtual endpoints that can be used to process incoming connections
+ /// </summary>
+ public interface IVirtualEndpointTable
+ {
+ /// <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>
+ void AddEndpoint(params IEndpoint[] endpoints);
+
+ /// <summary>
+ /// Removes the specified endpoint from the virtual endpoint store
+ /// </summary>
+ /// <param name="eps">A collection of endpoints to remove from the table</param>
+ void RemoveEndpoint(params IEndpoint[] eps);
+
+ /// <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>
+ void RemoveEndpoint(params string[] paths);
+
+ /// <summary>
+ /// A value that indicates whether the table is empty, allows for quick checks
+ /// without causing lookups
+ /// </summary>
+ bool IsEmpty { get; }
+
+ /// <summary>
+ /// Attempts to get the endpoint associated with the specified path
+ /// </summary>
+ /// <param name="path">The connection path to recover the endpoint from</param>
+ /// <param name="endpoint"></param>
+ /// <returns></returns>
+ bool TryGetEndpoint(string path, out IVirtualEndpoint<HttpEntity>? endpoint);
+ }
+} \ No newline at end of file
diff --git a/lib/Plugins.Essentials/src/Middleware/HttpMiddlewareResult.cs b/lib/Plugins.Essentials/src/Middleware/HttpMiddlewareResult.cs
new file mode 100644
index 0000000..6054a6e
--- /dev/null
+++ b/lib/Plugins.Essentials/src/Middleware/HttpMiddlewareResult.cs
@@ -0,0 +1,42 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: HttpMiddlewareResult.cs
+*
+* HttpMiddlewareResult.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.Middleware
+{
+ /// <summary>
+ /// The result of a <see cref="IHttpMiddleware"/> process.
+ /// </summary>
+ public enum HttpMiddlewareResult
+ {
+ /// <summary>
+ /// The request has not been completed and should continue to be processed.
+ /// </summary>
+ Continue,
+
+ /// <summary>
+ /// The request has been handled and no further processing should occur.
+ /// </summary>
+ Complete
+ }
+}
diff --git a/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs b/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs
new file mode 100644
index 0000000..a5a3949
--- /dev/null
+++ b/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs
@@ -0,0 +1,44 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: IHttpMiddleware.cs
+*
+* IHttpMiddleware.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;
+
+
+namespace VNLib.Plugins.Essentials.Middleware
+{
+ /// <summary>
+ /// Represents a low level intermediate request processor with high privilages, meant to add
+ /// functionality to entity processing.
+ /// </summary>
+ public interface IHttpMiddleware
+ {
+ /// <summary>
+ /// Processes the <see cref="HttpEntity"/> and returns a <see cref="HttpMiddlewareResult"/>
+ /// indicating whether the request should continue to be processed.
+ /// </summary>
+ /// <param name="entity">The entity to process</param>
+ /// <returns>The result of the operation</returns>
+ ValueTask<HttpMiddlewareResult> ProcessAsync(HttpEntity entity);
+ }
+}
diff --git a/lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.cs b/lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.cs
new file mode 100644
index 0000000..54da6c1
--- /dev/null
+++ b/lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.cs
@@ -0,0 +1,67 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: IHttpMiddlewareChain.cs
+*
+* IHttpMiddlewareChain.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.Collections.Generic;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Middleware
+{
+ /// <summary>
+ /// Represents a chain of <see cref="IHttpMiddleware"/> instances that
+ /// will be called by an <see cref="EventProcessor"/> during
+ /// entity processing.
+ /// </summary>
+ public interface IHttpMiddlewareChain
+ {
+ /// <summary>
+ /// Gets the current head of the middleware chain
+ /// </summary>
+ /// <returns>A <see cref="LinkedListNode{T}"/> that points to the head of the current chain</returns>
+ LinkedListNode<IHttpMiddleware>? GetCurrentHead();
+
+ /// <summary>
+ /// Adds a middleware handler to the end of the chain
+ /// </summary>
+ /// <param name="middleware">The middleware processor to add</param>
+ void AddLast(IHttpMiddleware middleware);
+
+ /// <summary>
+ /// Adds a middleware handler to the beginning of the chain
+ /// </summary>
+ /// <param name="middleware">The middleware processor to add</param>
+ void AddFirst(IHttpMiddleware middleware);
+
+ /// <summary>
+ /// Removes a middleware handler from the chain
+ /// </summary>
+ /// <param name="middleware">The middleware instance to remove</param>
+ void RemoveMiddleware(IHttpMiddleware middleware);
+
+ /// <summary>
+ /// Removes all middleware handlers from the chain
+ /// </summary>
+ void Clear();
+ }
+} \ No newline at end of file
diff --git a/lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs b/lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs
new file mode 100644
index 0000000..197ba12
--- /dev/null
+++ b/lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs
@@ -0,0 +1,86 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: SemiConistentMiddlewareChain.cs
+*
+* SemiConistentMiddlewareChain.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.Collections.Generic;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Middleware
+{
+ /// <summary>
+ /// A default implementation of <see cref="IHttpMiddlewareChain"/> that
+ /// maintains a semi-conistant chain of middleware handlers, for infrequent
+ /// chain updates
+ /// </summary>
+ internal sealed class SemiConistentMiddlewareChain : IHttpMiddlewareChain
+ {
+ private LinkedList<IHttpMiddleware> _middlewares = new();
+
+ ///<inheritdoc/>
+ public void AddFirst(IHttpMiddleware middleware)
+ {
+ lock (_middlewares)
+ {
+ _middlewares.AddFirst(middleware);
+ }
+ }
+
+ ///<inheritdoc/>
+ public void AddLast(IHttpMiddleware middleware)
+ {
+ lock (_middlewares)
+ {
+ _middlewares.AddLast(middleware);
+ }
+ }
+
+ ///<inheritdoc/>
+ public void Clear()
+ {
+ lock (_middlewares)
+ {
+ _middlewares.Clear();
+ }
+ }
+
+ ///<inheritdoc/>
+ public LinkedListNode<IHttpMiddleware>? GetCurrentHead() => _middlewares.First;
+
+ ///<inheritdoc/>
+ public void RemoveMiddleware(IHttpMiddleware middleware)
+ {
+ lock (_middlewares)
+ {
+ //Clone current table
+ LinkedList<IHttpMiddleware> newTable = new(_middlewares);
+
+ //Remove the middleware
+ newTable.Remove(middleware);
+
+ //Replace the current table with the new one
+ _middlewares = newTable;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs b/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs
new file mode 100644
index 0000000..d43432a
--- /dev/null
+++ b/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs
@@ -0,0 +1,175 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: SemiConsistentVeTable.cs
+*
+* SemiConsistentVeTable.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.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using VNLib.Net.Http;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials
+{
+ internal class SemiConsistentVeTable : IVirtualEndpointTable
+ {
+
+ /*
+ * 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 IReadOnlyDictionary<string, IVirtualEndpoint<HttpEntity>> VirtualEndpoints = new Dictionary<string, IVirtualEndpoint<HttpEntity>>(StringComparer.OrdinalIgnoreCase);
+
+
+ /*
+ * A lock that is held by callers that intend to
+ * modify the vep table at the same time
+ */
+ private readonly object VeUpdateLock = new();
+
+ ///<inheritdoc/>
+ public bool IsEmpty => VirtualEndpoints.Count == 0;
+
+
+ ///<inheritdoc/>
+ 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);
+ }
+ }
+
+ ///<inheritdoc/>
+ public void RemoveEndpoint(params IEndpoint[] eps)
+ {
+ _ = eps ?? throw new ArgumentNullException(nameof(eps));
+ //Call remove on path
+ RemoveEndpoint(eps.Select(static s => s.Path).ToArray());
+ }
+
+ ///<inheritdoc/>
+ public void RemoveEndpoint(params string[] paths)
+ {
+ _ = paths ?? throw new ArgumentNullException(nameof(paths));
+
+ //Make sure all endpoints specify a path
+ if (paths.Any(static e => string.IsNullOrWhiteSpace(e)))
+ {
+ 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
+ _ = Interlocked.Exchange(ref VirtualEndpoints, newTable);
+ }
+ }
+
+ ///<inheritdoc/>
+ public bool TryGetEndpoint(string path, out IVirtualEndpoint<HttpEntity>? endpoint) => VirtualEndpoints.TryGetValue(path, out endpoint);
+
+
+ /*
+ * Wrapper class for converting IHttpEvent endpoints to
+ * httpEntityEndpoints
+ */
+ private sealed record class EvEndpointWrapper(IVirtualEndpoint<IHttpEvent> Wrapped) : IVirtualEndpoint<HttpEntity>
+ {
+ string IEndpoint.Path => Wrapped.Path;
+ ValueTask<VfReturnType> IVirtualEndpoint<HttpEntity>.Process(HttpEntity entity) => Wrapped.Process(entity);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Plugins.Essentials/src/WebSocketSession.cs b/lib/Plugins.Essentials/src/WebSocketSession.cs
index 106501c..c43a876 100644
--- a/lib/Plugins.Essentials/src/WebSocketSession.cs
+++ b/lib/Plugins.Essentials/src/WebSocketSession.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -44,17 +44,30 @@ namespace VNLib.Plugins.Essentials
/// will be closed and the session disposed
/// </returns>
- public delegate Task WebsocketAcceptedCallback(WebSocketSession session);
+ public delegate Task WebSocketAcceptedCallback(WebSocketSession session);
+
+ /// <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>
+ /// <typeparam name="T">The type of the user state object</typeparam>
+ /// <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<T>(WebSocketSession<T> 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
+ public class WebSocketSession : AlternateProtocolBase
{
- private WebSocket? WsHandle;
- private readonly WebsocketAcceptedCallback AcceptedCallback;
+ internal WebSocket? WsHandle;
+ internal readonly WebSocketAcceptedCallback AcceptedCallback;
/// <summary>
/// A cancellation token that can be monitored to reflect the state
@@ -70,21 +83,16 @@ namespace VNLib.Plugins.Essentials
/// <summary>
/// Negotiated sub-protocol
/// </summary>
- public string? SubProtocol { get; }
-
+ public string? SubProtocol { get; internal init; }
+
/// <summary>
- /// A user-defined state object passed during socket accept handshake
+ /// The websocket keep-alive interval
/// </summary>
- public object? UserState { get; internal set; }
-
- internal WebSocketSession(string? subProtocol, WebsocketAcceptedCallback callback)
- : this(Guid.NewGuid().ToString("N"), subProtocol, callback)
- { }
+ internal TimeSpan KeepAlive { get; init; }
- internal WebSocketSession(string socketId, string? subProtocol, WebsocketAcceptedCallback callback)
+ internal WebSocketSession(string socketId, WebSocketAcceptedCallback callback)
{
SocketID = socketId;
- SubProtocol = subProtocol;
//Store the callback function
AcceptedCallback = callback;
}
@@ -101,7 +109,7 @@ namespace VNLib.Plugins.Essentials
WebSocketCreationOptions ce = new()
{
IsServer = true,
- KeepAliveInterval = TimeSpan.FromSeconds(30),
+ KeepAliveInterval = KeepAlive,
SubProtocol = SubProtocol,
};
@@ -117,7 +125,6 @@ namespace VNLib.Plugins.Essentials
finally
{
WsHandle?.Dispose();
- UserState = null;
}
}
@@ -158,7 +165,8 @@ namespace VNLib.Plugins.Essentials
//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>
@@ -170,7 +178,21 @@ namespace VNLib.Plugins.Essentials
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);
+ return SendAsync(buffer, type, endOfMessage ? WebSocketMessageFlags.EndOfMessage : WebSocketMessageFlags.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="flags">Websocket message flags</param>
+ /// <returns></returns>
+ /// <exception cref="OperationCanceledException"></exception>
+ public ValueTask SendAsync(ReadOnlyMemory<byte> buffer, WebSocketMessageType type, WebSocketMessageFlags flags)
+ {
+ //Create a send request with
+ return WsHandle!.SendAsync(buffer, type, flags, CancellationToken.None);
}
@@ -201,4 +223,27 @@ namespace VNLib.Plugins.Essentials
return Task.CompletedTask;
}
}
+
+ /// <summary>
+ /// <inheritdoc/>
+ /// </summary>
+ /// <typeparam name="T">The user-state type</typeparam>
+ public sealed class WebSocketSession<T> : WebSocketSession
+ {
+
+#nullable disable
+
+ /// <summary>
+ /// A user-defined state object passed during socket accept handshake
+ /// </summary>
+ public T UserState { get; internal init; }
+
+#nullable enable
+
+ internal WebSocketSession(string sessionId, WebSocketAcceptedCallback<T> callback)
+ : base(sessionId, (ses) => callback((ses as WebSocketSession<T>)!))
+ {
+ UserState = default;
+ }
+ }
} \ No newline at end of file