aboutsummaryrefslogtreecommitdiff
path: root/lib/Plugins.Essentials/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Plugins.Essentials/src')
-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
9 files changed, 680 insertions, 194 deletions
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