diff options
Diffstat (limited to 'lib')
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 |