From 1b590c2517fef110564943ed8a10edd11fa758b0 Mon Sep 17 00:00:00 2001 From: vnugent Date: Wed, 22 May 2024 17:49:57 -0400 Subject: Squashed commit of the following: commit 9a835fe12c9586ab8dd44d7c96fef4a2d6017e4b Author: vnugent Date: Fri May 17 18:27:03 2024 -0400 chore: Update mimmaloc v2.1.6, update fPIC & cleanup commit 3b7004b88acfc7f7baa3a8857a5a2f7cf3dd560e Author: vnugent Date: Fri May 17 16:03:28 2024 -0400 feat: Added ReadFileDataAsync function commit 9a964795757bf0da4dd7fcab15ad304f4ea3fdf1 Author: vnugent Date: Wed May 15 21:57:39 2024 -0400 refactor: Harden some argon2 password hashing commit 4035c838c1508af0aa7e767a97431a692958ce1c Author: vnugent Date: Sun May 12 16:55:32 2024 -0400 perf: Utils + http perf mods commit f4f0d4f74250257991c57bfae74c4852c7e1ae46 Author: vnugent Date: Thu May 2 15:22:53 2024 -0400 feat: Buff middleware handlers | | Added implicit support for middleware post processing of files before the filehandler closes the connection. Also cleaned up some project file stuff commit f0b7dca107659df1d7d4631fdbd2aae9d716d053 Merge: 8c4a45e 107b058 Author: vnugent Date: Sat Apr 20 12:24:05 2024 -0400 Merge branch 'main' into develop commit 8c4a45e384accf92b1b6d748530e8d46f7de40d6 Author: vnugent Date: Sat Apr 20 11:10:30 2024 -0400 refactor: Overhaul C libraries and fix builds commit 42ff77080d10b0fc9fecbbc46141e8e23a1d066a Author: vnugent Date: Sat Apr 20 00:45:57 2024 -0400 fix!: Middlware array, multiple cookie set, and cookie check commit 97e82b9d66f387f9e6d21d88ddc7a8ab8693149c Merge: 4ca5791 e07537a Author: vnugent Date: Tue Apr 2 13:34:22 2024 -0400 Merge branch 'main' into develop commit 4ca5791ed67b9834bdbd010206b30373e4705e9b Author: vnugent Date: Tue Apr 2 13:32:12 2024 -0400 fix: Missed ! on null pointer check commit 9b4036377c52200c6488c98180d69a0e63321f97 Author: vnugent Date: Tue Apr 2 13:22:29 2024 -0400 fix: Fix _In_ macro for compression public api commit 53a7b4b5c5b67b4a4e06e1d9098cac4bcd6afd7c Merge: 448a93b 21130c8 Author: vnugent Date: Sun Mar 31 17:01:15 2024 -0400 Merge branch 'main' into develop commit 448a93bb1d18d032087afe2476ffccb98634a54c Author: vnugent Date: Sun Mar 31 16:56:51 2024 -0400 ci: fix third-party dir cleanup commit 9afed1427472da1ea13079f98dbe27339e55ee7d Author: vnugent Date: Sun Mar 31 16:43:15 2024 -0400 perf: Deprecate unsafememoryhandle span extensions commit 3ff90da4f02af47ea6d233fdd4445337ebe36452 Author: vnugent Date: Sat Mar 30 21:36:18 2024 -0400 refactor: Updates, advanced tracing, http optimizations commit 8d6b79b5ae309b36f265ba81529bcef8bfcd7414 Merge: 6c1667b 5585915 Author: vnugent Date: Sun Mar 24 21:01:31 2024 -0400 Merge branch 'main' into develop commit 6c1667be23597513537f8190e2f55d65eb9b7c7a Author: vnugent Date: Fri Mar 22 12:01:53 2024 -0400 refactor: Overhauled native library loading and lazy init commit ebf688f2f974295beabf7b5def7e6f6f150551d0 Author: vnugent Date: Wed Mar 20 22:16:17 2024 -0400 refactor: Update compression header files and macros + Ci build commit 9c7b564911080ccd5cbbb9851a0757b05e1e9047 Author: vnugent Date: Tue Mar 19 21:54:49 2024 -0400 refactor: JWK overhaul & add length getter to FileUpload commit 6d8c3444e09561e5957491b3cc1ae858e0abdd14 Author: vnugent Date: Mon Mar 18 16:13:20 2024 -0400 feat: Add FNV1a software checksum and basic correction tests commit 00d182088cecefc08ca80b1faee9bed3f215f40b Author: vnugent Date: Fri Mar 15 01:05:27 2024 -0400 chore: #6 Use utils filewatcher instead of built-in commit d513c10d9895c6693519ef1d459c6a5a76929436 Author: vnugent Date: Sun Mar 10 21:58:14 2024 -0400 source tree project location updated --- .../src/Accounts/PasswordHashing.cs | 100 +++++++---- .../src/Endpoints/SemiConsistentVeTable.cs | 187 ++++++++++++++++++++ lib/Plugins.Essentials/src/EventProcessor.cs | 37 ++-- lib/Plugins.Essentials/src/EventProcessorConfig.cs | 1 + .../src/Extensions/ConnectionInfoExtensions.cs | 131 ++++++++------ .../src/Extensions/EssentialHttpEventExtensions.cs | 64 ++++++- .../src/Extensions/HttpCookie.cs | 3 +- .../src/Extensions/SingleCookieController.cs | 18 +- lib/Plugins.Essentials/src/HttpEntity.cs | 54 ++++++ .../src/Middleware/IHttpMiddleware.cs | 16 +- .../src/Middleware/MiddlewareController.cs | 88 ++++++++++ .../src/Oauth/OauthHttpExtensions.cs | 24 +-- .../src/SemiConsistentVeTable.cs | 188 --------------------- .../src/VNLib.Plugins.Essentials.csproj | 8 +- 14 files changed, 603 insertions(+), 316 deletions(-) create mode 100644 lib/Plugins.Essentials/src/Endpoints/SemiConsistentVeTable.cs create mode 100644 lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs delete mode 100644 lib/Plugins.Essentials/src/SemiConsistentVeTable.cs (limited to 'lib/Plugins.Essentials') diff --git a/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs b/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs index e6b9f24..6c5ebcc 100644 --- a/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs +++ b/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -29,6 +29,15 @@ using VNLib.Hashing; using VNLib.Utils; using VNLib.Utils.Memory; +/* + * Some stuff to note + * + * Functions have explicit parameters to avoid accidental buffer mixup + * when calling nested/overload functions. Please keep it that way for now + * I really want to avoid a whoopsie in password hasing. + */ + + namespace VNLib.Plugins.Essentials.Accounts { @@ -59,7 +68,8 @@ namespace VNLib.Plugins.Essentials.Accounts /// The password secret provider /// The configuration setup arguments /// The instance of the library to use - public static PasswordHashing Create(IArgon2Library library, ISecretProvider secret, in Argon2ConfigParams setup) => new (library, secret, setup); + public static PasswordHashing Create(IArgon2Library library, ISecretProvider secret, in Argon2ConfigParams setup) + => new (library, secret, in setup); /// /// Creates a new instance using the default @@ -69,7 +79,8 @@ namespace VNLib.Plugins.Essentials.Accounts /// The configuration setup arguments /// The instance of the library to use /// - public static PasswordHashing Create(ISecretProvider secret, in Argon2ConfigParams setup) => Create(VnArgon2.GetOrLoadSharedLib(), secret, in setup); + public static PasswordHashing Create(ISecretProvider secret, in Argon2ConfigParams setup) + => Create(VnArgon2.GetOrLoadSharedLib(), secret, in setup); private Argon2CostParams GetCostParams() { @@ -93,15 +104,18 @@ namespace VNLib.Plugins.Essentials.Accounts if(_secret.BufferSize < STACK_MAX_BUFF_SIZE) { - //Alloc stack buffer + /* + * Also always alloc fixed buffer size again to help + * be less obvious during process allocations + */ Span secretBuffer = stackalloc byte[STACK_MAX_BUFF_SIZE]; return VerifyInternal(passHash, password, secretBuffer); } else { - //Alloc heap buffer - using UnsafeMemoryHandle secretBuffer = MemoryUtil.UnsafeAlloc(_secret.BufferSize, true); + + using UnsafeMemoryHandle secretBuffer = AllocSecretBuffer(); return VerifyInternal(passHash, password, secretBuffer.Span); } @@ -111,10 +125,10 @@ namespace VNLib.Plugins.Essentials.Accounts { try { - //Get the secret from the callback - ERRNO count = _secret.GetSecret(secretBuffer); - //Verify - return _argon2.Verify2id(password, passHash, secretBuffer[..(int)count]); + + ERRNO secretSize = _secret.GetSecret(secretBuffer); + + return _argon2.Verify2id(password, passHash, secretBuffer[..(int)secretSize]); } finally { @@ -135,11 +149,9 @@ namespace VNLib.Plugins.Essentials.Accounts public bool Verify(ReadOnlySpan hash, ReadOnlySpan salt, ReadOnlySpan password) { if (hash.Length < STACK_MAX_BUFF_SIZE) - { - //Alloc stack buffer + { Span hashBuf = stackalloc byte[hash.Length]; - - //Hash the password with the current config + Hash(password, salt, hashBuf); //Compare the hashed password to the specified hash and return results @@ -148,8 +160,7 @@ namespace VNLib.Plugins.Essentials.Accounts else { using UnsafeMemoryHandle hashBuf = MemoryUtil.UnsafeAlloc(hash.Length, true); - - //Hash the password with the current config + Hash(password, salt, hashBuf.Span); //Compare the hashed password to the specified hash and return results @@ -165,7 +176,7 @@ namespace VNLib.Plugins.Essentials.Accounts Argon2CostParams costParams = GetCostParams(); //Alloc shared buffer for the salt and secret buffer - using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(_config.SaltLen + _secret.BufferSize, true); + using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAllocNearestPage(_config.SaltLen + _secret.BufferSize, true); //Split buffers Span saltBuf = buffer.Span[.._config.SaltLen]; @@ -175,12 +186,16 @@ namespace VNLib.Plugins.Essentials.Accounts RandomHash.GetRandomBytes(saltBuf); try - { - //recover the secret + { ERRNO count = _secret.GetSecret(secretBuf); - - //Hashes a password, with the current parameters - return (PrivateString)_argon2.Hash2id(password, saltBuf, secretBuf[..(int)count], in costParams, _config.HashLen); + + return (PrivateString)_argon2.Hash2id( + password: password, + salt: saltBuf, + secret: secretBuf[..(int)count], + costParams: in costParams, + hashLen: _config.HashLen + ); } finally { @@ -201,7 +216,10 @@ namespace VNLib.Plugins.Essentials.Accounts Span saltBuf = buffer.Span[.._config.SaltLen]; Span secretBuf = buffer.Span[_config.SaltLen..]; - //Fill the buffer with random bytes + /* + * Salt is just crypographically secure random + * data. + */ RandomHash.GetRandomBytes(saltBuf); try @@ -210,7 +228,13 @@ namespace VNLib.Plugins.Essentials.Accounts ERRNO count = _secret.GetSecret(secretBuf); //Hashes a password, with the current parameters - return (PrivateString)_argon2.Hash2id(password, saltBuf, secretBuf[..(int)count], in costParams, _config.HashLen); + return (PrivateString)_argon2.Hash2id( + password: password, + salt: saltBuf, + secret: secretBuf[..(int)count], + costParams: in costParams, + hashLen: _config.HashLen + ); } finally { @@ -229,20 +253,28 @@ namespace VNLib.Plugins.Essentials.Accounts public void Hash(ReadOnlySpan password, ReadOnlySpan salt, Span hashOutput) { Argon2CostParams costParams = GetCostParams(); + + using UnsafeMemoryHandle secretBuffer = AllocSecretBuffer(); - //alloc secret buffer - using UnsafeMemoryHandle secretBuffer = MemoryUtil.UnsafeAllocNearestPage(_secret.BufferSize, true); try { - //Get the secret from the callback - ERRNO count = _secret.GetSecret(secretBuffer.Span); - //Hashes a password, with the current parameters - _argon2.Hash2id(password, salt, secretBuffer.Span[..(int)count], hashOutput, in costParams); + ERRNO secretSize = _secret.GetSecret(secretBuffer.Span); + + _argon2.Hash2id( + password: password, + salt: salt, + secret: secretBuffer.AsSpan(0, secretSize), + rawHashOutput: hashOutput, + costParams: in costParams + ); } finally { //Erase secret buffer - MemoryUtil.InitializeBlock(ref secretBuffer.GetReference(), secretBuffer.IntLength); + MemoryUtil.InitializeBlock( + ref secretBuffer.GetReference(), + secretBuffer.IntLength + ); } } @@ -290,6 +322,12 @@ namespace VNLib.Plugins.Essentials.Accounts } } + /* + * Always alloc page aligned to help keep block allocations + * a little less obvious. + */ + private UnsafeMemoryHandle AllocSecretBuffer() => MemoryUtil.UnsafeAllocNearestPage(_secret.BufferSize, true); + private readonly ref struct HashBufferSegments { public readonly Span SaltBuffer; diff --git a/lib/Plugins.Essentials/src/Endpoints/SemiConsistentVeTable.cs b/lib/Plugins.Essentials/src/Endpoints/SemiConsistentVeTable.cs new file mode 100644 index 0000000..09ab151 --- /dev/null +++ b/lib/Plugins.Essentials/src/Endpoints/SemiConsistentVeTable.cs @@ -0,0 +1,187 @@ +/* +* Copyright (c) 2024 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.Frozen; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using VNLib.Net.Http; + +namespace VNLib.Plugins.Essentials.Endpoints +{ + internal sealed 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. + */ + + /// + /// A "lookup table" that represents virtual endpoints to be processed when an + /// incomming connection matches its path parameter + /// + private FrozenDictionary> VirtualEndpoints = + new Dictionary>(StringComparer.OrdinalIgnoreCase) + .ToFrozenDictionary(); + + private bool _isEmpty = true; + + /* + * A lock that is held by callers that intend to + * modify the vep table at the same time + */ + private readonly object VeUpdateLock = new(); + + /// + public bool IsEmpty => _isEmpty; + + + /// + public void AddEndpoint(params IEndpoint[] endpoints) + { + //Check + ArgumentNullException.ThrowIfNull(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> eps = endpoints + .Where(static e => e is IVirtualEndpoint) + .Select(static e => (IVirtualEndpoint)e); + + //Get http event endpoints and create wrapper classes for conversion + IEnumerable> evs = endpoints + .Where(static e => e is IVirtualEndpoint) + .Select(static e => new EvEndpointWrapper((e as IVirtualEndpoint)!)); + + //Uinion endpoints by their paths to combine them + IEnumerable> allEndpoints = eps.UnionBy(evs, static s => s.Path); + + //Only allow 1 thread at a time to mutate the table + lock (VeUpdateLock) + { + //Clone the current dictonary + Dictionary> newTable = new(VirtualEndpoints, StringComparer.OrdinalIgnoreCase); + //Insert the new eps, and/or overwrite old eps + foreach (IVirtualEndpoint ep in allEndpoints) + { + newTable.Add(ep.Path, ep); + } + + //Update is-empty flag + _isEmpty = newTable.Count == 0; + + //Create the new table and store the entire table + _ = Interlocked.Exchange(ref VirtualEndpoints, newTable.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase)); + } + } + + /// + public void RemoveEndpoint(params IEndpoint[] eps) + { + ArgumentNullException.ThrowIfNull(eps); + //Call remove on path + RemoveEndpoint(eps.Select(static s => s.Path).ToArray()); + } + + /// + public void RemoveEndpoint(params string[] paths) + { + ArgumentNullException.ThrowIfNull(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> newTable = new(VirtualEndpoints, StringComparer.OrdinalIgnoreCase); + + foreach (string eps in paths) + { + _ = newTable.Remove(eps); + } + + //Update is-empty flag + _isEmpty = newTable.Count == 0; + + //Store the new table + _ = Interlocked.Exchange(ref VirtualEndpoints, newTable.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase)); + } + } + + /// + public bool TryGetEndpoint(string path, [NotNullWhen(true)] out IVirtualEndpoint? endpoint) + => VirtualEndpoints.TryGetValue(path, out endpoint); + + + /* + * Wrapper class for converting IHttpEvent endpoints to + * httpEntityEndpoints + */ + private sealed class EvEndpointWrapper(IVirtualEndpoint Wrapped) : IVirtualEndpoint + { + string IEndpoint.Path => Wrapped.Path; + + ValueTask IVirtualEndpoint.Process(HttpEntity entity) => Wrapped.Process(entity); + } + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/EventProcessor.cs b/lib/Plugins.Essentials/src/EventProcessor.cs index f052c56..ba7aa3c 100644 --- a/lib/Plugins.Essentials/src/EventProcessor.cs +++ b/lib/Plugins.Essentials/src/EventProcessor.cs @@ -28,7 +28,6 @@ using System.Net; using System.Threading; using System.Net.Sockets; using System.Threading.Tasks; -using System.Collections.Generic; using System.Collections.Immutable; using System.Runtime.CompilerServices; @@ -40,8 +39,8 @@ 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; using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Middleware; #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task @@ -133,7 +132,8 @@ namespace VNLib.Plugins.Essentials /// The internal service pool for the processor /// protected readonly HttpProcessorServicePool ServicePool = new([ - typeof(ISessionProvider), //Order must match the indexes above + //Order must match the indexes above + typeof(ISessionProvider), typeof(IPageRouter), typeof(IAccountSecurityProvider) ]); @@ -157,6 +157,8 @@ namespace VNLib.Plugins.Essentials get => ServicePool.ExchangeVersion(ref _accountSec, SEC_INDEX); } + private readonly MiddlewareController _middleware = new(config); + /// public virtual async ValueTask ClientConnectedAsync(IHttpEvent httpEvent) { @@ -169,8 +171,6 @@ namespace VNLib.Plugins.Essentials ISessionProvider? sessions = ServicePool.ExchangeVersion(ref _sessions, SESS_INDEX); IPageRouter? router = ServicePool.ExchangeVersion(ref _router, ROUTER_INDEX); - LinkedListNode? mwNode = config.MiddlewareChain.GetCurrentHead(); - //event cancellation token HttpEntity entity = new(httpEvent, this); @@ -205,24 +205,10 @@ namespace VNLib.Plugins.Essentials goto RespondAndExit; } - //Loop through nodes - while(mwNode != null) + //Exec middleware + if(!await _middleware.ProcessAsync(entity)) { - //Invoke mw handler on our event - entity.EventArgs = await mwNode.ValueRef.ProcessAsync(entity); - - switch (entity.EventArgs.Routine) - { - //move next if continue is returned - case FpRoutine.Continue: - break; - - //Middleware completed the connection, time to exit - default: - goto RespondAndExit; - } - - mwNode = mwNode.Next; + goto RespondAndExit; } if (!config.EndpointTable.IsEmpty) @@ -257,6 +243,9 @@ namespace VNLib.Plugins.Essentials RespondAndExit: + //Normal post-process + _middleware.PostProcess(entity); + //Call post processor method PostProcessEntity(entity, ref entity.EventArgs); } @@ -744,5 +733,5 @@ namespace VNLib.Plugins.Essentials return arr; } } - } -} \ No newline at end of file + } +} diff --git a/lib/Plugins.Essentials/src/EventProcessorConfig.cs b/lib/Plugins.Essentials/src/EventProcessorConfig.cs index 8f401ac..6e101eb 100644 --- a/lib/Plugins.Essentials/src/EventProcessorConfig.cs +++ b/lib/Plugins.Essentials/src/EventProcessorConfig.cs @@ -29,6 +29,7 @@ using System.Collections.Frozen; using System.Collections.Generic; using VNLib.Utils.Logging; +using VNLib.Plugins.Essentials.Endpoints; using VNLib.Plugins.Essentials.Middleware; namespace VNLib.Plugins.Essentials diff --git a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs index a99b1ab..64a9611 100644 --- a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs +++ b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs @@ -114,6 +114,7 @@ namespace VNLib.Plugins.Essentials.Extensions /// true if the connection accepts any content typ, false otherwise private static bool AcceptsAny(IConnectionInfo server) { + // If no accept header is sent by clients, it is assumed it accepts all content types if(server.Accept.Count == 0) { return true; @@ -196,14 +197,15 @@ namespace VNLib.Plugins.Essentials.Extensions //Alloc enough space to hold the string Span buffer = stackalloc char[64]; ForwardOnlyWriter rangeBuilder = new(buffer); + //Build the range header in this format "bytes -/" - rangeBuilder.Append("bytes "); + rangeBuilder.AppendSmall("bytes "); rangeBuilder.Append(start); rangeBuilder.Append('-'); rangeBuilder.Append(end); rangeBuilder.Append('/'); rangeBuilder.Append(length); - //Print to a string and set the content range header + entity.Server.Headers[HttpResponseHeader.ContentRange] = rangeBuilder.ToString(); } @@ -212,7 +214,8 @@ namespace VNLib.Plugins.Essentials.Extensions /// /// true if the user-agent specified the cors security header [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsCors(this IConnectionInfo server) => "cors".Equals(server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase); + public static bool IsCors(this IConnectionInfo server) + => string.Equals("cors", server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase); /// /// Determines if the User-Agent specified "cross-site" in the Sec-Site header, OR @@ -223,8 +226,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsCrossSite(this IConnectionInfo server) { - return "cross-site".Equals(server.Headers[SEC_HEADER_SITE], StringComparison.OrdinalIgnoreCase) - || (server.Origin != null && !server.RequestUri.DnsSafeHost.Equals(server.Origin.DnsSafeHost, StringComparison.Ordinal)); + return string.Equals("cross-site", server.Headers[SEC_HEADER_SITE], StringComparison.OrdinalIgnoreCase) + || (server.Origin != null && ! string.Equals(server.RequestUri.DnsSafeHost, server.Origin.DnsSafeHost, StringComparison.Ordinal)); } /// @@ -233,14 +236,16 @@ namespace VNLib.Plugins.Essentials.Extensions /// /// true if sec-user header was set to "?1" [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsUserInvoked(this IConnectionInfo server) => "?1".Equals(server.Headers[SEC_HEADER_USER], StringComparison.OrdinalIgnoreCase); + public static bool IsUserInvoked(this IConnectionInfo server) + => string.Equals("?1", server.Headers[SEC_HEADER_USER], StringComparison.OrdinalIgnoreCase); /// /// Was this request created from normal user navigation /// /// true if sec-mode set to "navigate" [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsNavigation(this IConnectionInfo server) => "navigate".Equals(server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase); + public static bool IsNavigation(this IConnectionInfo server) + => string.Equals("navigate", server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase); /// /// Determines if the client specified "no-cache" for the cache control header, signalling they do not wish to cache the entity @@ -302,7 +307,11 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool RefererMatch(this IConnectionInfo server) { - return server.RequestUri.DnsSafeHost.Equals(server.Referer?.DnsSafeHost, StringComparison.OrdinalIgnoreCase); + return string.Equals( + server.RequestUri.DnsSafeHost, + server.Referer?.DnsSafeHost, + StringComparison.OrdinalIgnoreCase + ); } /// @@ -315,9 +324,25 @@ namespace VNLib.Plugins.Essentials.Extensions /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ExpireCookie(this IConnectionInfo server, string name, string domain = "", string path = "/", CookieSameSite sameSite = CookieSameSite.None, bool secure = false) + public static void ExpireCookie( + this IConnectionInfo server, + string name, + string domain = "", + string path = "/", + CookieSameSite sameSite = CookieSameSite.None, + bool secure = false + ) { - server.SetCookie(name, string.Empty, domain, path, TimeSpan.Zero, sameSite, false, secure); + SetCookie( + server: server, + name: name, + value: string.Empty, + domain: domain, + path: path, + expires: TimeSpan.Zero, + sameSite: sameSite, + secure: secure + ); } /// @@ -340,9 +365,20 @@ namespace VNLib.Plugins.Essentials.Extensions string path = "/", CookieSameSite sameSite = CookieSameSite.None, bool httpOnly = false, - bool secure = false) + bool secure = false + ) { - server.SetCookie(name, value, domain, path, TimeSpan.MaxValue, sameSite, httpOnly, secure); + SetCookie( + server: server, + name: name, + value: value, + domain: domain, + path: path, + expires: TimeSpan.Zero, + sameSite: sameSite, + httpOnly: httpOnly, + secure: secure + ); } /// @@ -367,9 +403,24 @@ namespace VNLib.Plugins.Essentials.Extensions string path = "/", CookieSameSite sameSite = CookieSameSite.None, bool httpOnly = false, - bool secure = false) + bool secure = false + ) { - server.SetCookie(name, value, domain, path, expires, sameSite, httpOnly, secure); + + HttpResponseCookie cookie = new(name) + { + Value = value, + Domain = domain, + Path = path, + MaxAge = expires, + IsSession = expires == TimeSpan.MaxValue, + //If the connection is cross origin, then we need to modify the secure and samsite values + SameSite = sameSite, + HttpOnly = httpOnly, + Secure = secure | server.CrossOrigin, + }; + + server.SetCookie(in cookie); } @@ -380,35 +431,24 @@ namespace VNLib.Plugins.Essentials.Extensions /// The cookie to set for the server /// [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete("HttpCookie type is obsolete in favor of HttpResponseCookie")] public static void SetCookie(this IConnectionInfo server, in HttpCookie cookie) { - //Cookie name is required - if(string.IsNullOrWhiteSpace(cookie.Name)) - { - throw new ArgumentException("A nonn-null cookie name is required"); - } - //Set the cookie - server.SetCookie(cookie.Name, - cookie.Value, - cookie.Domain, - cookie.Path, - cookie.ValidFor, - cookie.SameSite, - cookie.HttpOnly, - cookie.Secure); - } - - /// - /// Is the current connection a "browser" ? - /// - /// - /// true if the user agent string contains "Mozilla" and does not contain "bot", false otherwise - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsBrowser(this IConnectionInfo server) - { - //Get user-agent and determine if its a browser - return server.UserAgent != null && !server.UserAgent.Contains("bot", StringComparison.OrdinalIgnoreCase) && server.UserAgent.Contains("Mozilla", StringComparison.OrdinalIgnoreCase); + HttpResponseCookie rCookie = new(cookie.Name) + { + Value = cookie.Value, + Domain = cookie.Domain, + Path = cookie.Path, + MaxAge = cookie.ValidFor, + IsSession = cookie.ValidFor == TimeSpan.MaxValue, + //If the connection is cross origin, then we need to modify the secure and samsite values + SameSite = cookie.SameSite, + HttpOnly = cookie.HttpOnly, + Secure = cookie.Secure | server.CrossOrigin, + }; + + server.SetCookie(in rCookie); } /// @@ -417,12 +457,9 @@ namespace VNLib.Plugins.Essentials.Extensions /// /// True of the connection was made from the local machine [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsLoopBack(this IConnectionInfo server) - { - IPAddress realIp = server.GetTrustedIp(); - return IPAddress.Any.Equals(realIp) || IPAddress.Loopback.Equals(realIp); - } - + public static bool IsLoopBack(this IConnectionInfo server) + => IPAddress.Loopback.Equals(GetTrustedIp(server)); + /// /// Did the connection set the dnt header? /// @@ -493,7 +530,7 @@ namespace VNLib.Plugins.Essentials.Extensions //Standard https protocol header string? protocol = server.Headers[X_FORWARDED_PROTO_HEADER]; //If the header is set and equals https then tls is being used - return string.IsNullOrWhiteSpace(protocol) ? isSecure : "https".Equals(protocol, StringComparison.OrdinalIgnoreCase); + return string.IsNullOrWhiteSpace(protocol) ? isSecure : string.Equals("https", protocol, StringComparison.OrdinalIgnoreCase); } else { diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs index 8adf883..0ca5b8f 100644 --- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs +++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs @@ -796,6 +796,66 @@ namespace VNLib.Plugins.Essentials.Extensions //Parse the file using the specified parser return parser(file.FileData, file.ContentTypeString()); } + + /// + /// Reads the contents of an uploaded file at the desired intex into memory + /// and returns a managed byte array containing the file data + /// + /// + /// The index of the uploaded file to buffer + /// A value task that resolves the uploaded data + /// + public static ValueTask ReadFileDataAsync(this HttpEntity ev, int uploadIndex = 0) + { + ArgumentNullException.ThrowIfNull(ev); + + /* + * File should exist at the desired index and have a length greater than 0 + * otherwise return an empty buffer + */ + if (ev.Files.Count <= uploadIndex || ev.Files[uploadIndex].Length == 0) + { + return ValueTask.FromResult(Array.Empty()); + } + + return ReadFileDataAsync(ev, uploadIndex); + + static async ValueTask ReadFileDataAsync(HttpEntity entity, int fileIndex) + { + FileUpload upload = entity.Files[fileIndex]; + + /* + * Alloc an uninitialized buffer to read the file data into, it should ALL + * be overwritten during read operation + */ + byte[] buffer = GC.AllocateUninitializedArray((int)upload.Length); + + int read = 0; + + do + { + Memory mem = buffer.AsMemory(read, buffer.Length - read); + + int r = await upload.FileData.ReadAsync(mem, entity.EventCancellation); + + //If no data was read force break and deal with read data + if (r == 0) + { + break; + } + + read += r; + } while (read < buffer.Length); + + //Buffer is exact length, so read should be equal to length + if (read != buffer.Length) + { + throw new IOException("Failed to read entire file data, this may be an internal error"); + } + + return buffer; + } + } /// /// Get a instance that points to the current sites filesystem root. @@ -816,12 +876,12 @@ namespace VNLib.Plugins.Essentials.Extensions public static string ContentTypeString(this in FileUpload upload) => HttpHelpers.GetContentTypeString(upload.ContentType); /// - /// Sets the flag on the current + /// Sets the flag on the current /// instance to disable dynamic compression on the response. /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void DisableCompression(this IHttpEvent entity) => entity.SetControlFlag(HttpControlMask.CompressionDisabed); + public static void DisableCompression(this IHttpEvent entity) => entity.SetControlFlag(HttpControlMask.CompressionDisabled); /// /// Attempts to upgrade the connection to a websocket, if the setup fails, it sets up the response to the client accordingly. diff --git a/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs b/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs index 6158a69..19c8e78 100644 --- a/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs +++ b/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -33,6 +33,7 @@ namespace VNLib.Plugins.Essentials.Extensions /// /// The cookie name /// The cookie value + [Obsolete("Obsolete in favor of HttpResponseCookie")] public readonly record struct HttpCookie (string Name, string Value) { /// diff --git a/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs b/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs index f3b02dc..ef86934 100644 --- a/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs +++ b/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs @@ -94,15 +94,19 @@ namespace VNLib.Plugins.Essentials.Extensions //Only set cooke if already exists or force is true if (entity.Server.RequestCookies.ContainsKey(Name) || force) { - //Build and set cookie - HttpCookie cookie = new(Name, value) + HttpResponseCookie cookie = new(Name) { - Secure = Secure, - HttpOnly = HttpOnly, - ValidFor = ValidFor, - SameSite = SameSite, + Value = value, + Domain = Domain, Path = Path, - Domain = Domain + //Only set max-age if cookie has a value, otherwise set to zero to expire + MaxAge = string.IsNullOrWhiteSpace(value) ? TimeSpan.Zero : ValidFor, + IsSession = ValidFor == TimeSpan.MaxValue, + SameSite = SameSite, + HttpOnly = HttpOnly, + + //Secure is required on cross origin requests + Secure = Secure | entity.Server.CrossOrigin, }; entity.Server.SetCookie(in cookie); diff --git a/lib/Plugins.Essentials/src/HttpEntity.cs b/lib/Plugins.Essentials/src/HttpEntity.cs index 2a24982..ff728e3 100644 --- a/lib/Plugins.Essentials/src/HttpEntity.cs +++ b/lib/Plugins.Essentials/src/HttpEntity.cs @@ -26,6 +26,7 @@ using System; using System.IO; using System.Net; using System.Threading; +using System.Diagnostics; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -204,6 +205,27 @@ namespace VNLib.Plugins.Essentials throw new ContentTypeUnacceptableException("The client does not accept the content type of the response"); } + /* + * If the underlying stream is actaully a memory stream, + * create a wrapper for it to read as a memory response. + * This is done to avoid a user-space copy since we can + * get access to access the internal buffer + * + * Stream length also should not cause an integer overflow, + * which also mean position is assumed not to overflow + * or cause an overflow during reading + */ + if(stream is MemoryStream ms && length < int.MaxValue) + { + Entity.CloseResponse( + code, + type, + new MemStreamWrapper(ms, (int)length) + ); + + return; + } + Entity.CloseResponse(code, type, stream, length); } @@ -246,5 +268,37 @@ namespace VNLib.Plugins.Essentials /// [MethodImpl(MethodImplOptions.AggressiveInlining)] void IHttpEvent.DangerousChangeProtocol(IAlternateProtocol protocolHandler) => Entity.DangerousChangeProtocol(protocolHandler); + + + private sealed class MemStreamWrapper(MemoryStream memStream, int length) : IMemoryResponseReader + { + readonly int length = length; + + /* + * Stream may be offset by the caller, it needs + * to be respected during streaming. + */ + int read = (int)memStream.Position; + + public int Remaining + { + get + { + Debug.Assert(length - read >= 0); + return length - read; + } + } + + public void Advance(int written) => read += written; + + /// + public void Close() => memStream.Dispose(); + + public ReadOnlyMemory GetMemory() + { + byte[] intBuffer = memStream.GetBuffer(); + return new ReadOnlyMemory(intBuffer, read, Remaining); + } + } } } diff --git a/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs b/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs index 83e6a06..990d59b 100644 --- a/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs +++ b/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -40,5 +40,19 @@ namespace VNLib.Plugins.Essentials.Middleware /// The entity to process /// The result of the operation ValueTask ProcessAsync(HttpEntity entity); + + /// + /// Post processes an HTTP entity with possible file selection. May optionally mutate the + /// current arguments before the event processor completes a response. + /// + /// The entity that has been processes and is ready to close + /// The current file processor arguments + /// + /// Generally this function should simply observe results as the entity may already have been + /// configured for a response, such as by a virtual routine. You should inspect the current arguments + /// before mutating the reference. + /// + virtual void VolatilePostProcess(HttpEntity entity, ref FileProcessArgs currentArgs) + { } } } diff --git a/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs b/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs new file mode 100644 index 0000000..c3a85c9 --- /dev/null +++ b/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs @@ -0,0 +1,88 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: MiddlewareController.cs +* +* MiddlewareController.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; +using System.Collections.Generic; + +namespace VNLib.Plugins.Essentials.Middleware +{ + internal sealed class MiddlewareController(EventProcessorConfig config) + { + private readonly IHttpMiddlewareChain _chain = config.MiddlewareChain; + + public async ValueTask ProcessAsync(HttpEntity entity) + { + /* + * Loops through the current linkedlist of the current middleware chain. The + * chain should remain unmodified after GetCurrentHead() is called. + * + * Middleware will return a Continue routine to move to the next middleware + * node. All other routines mean the processor has responded to the client + * itself and must exit control and move to response. + */ + + LinkedListNode? mwNode = _chain.GetCurrentHead(); + + //Loop through nodes + while (mwNode != null) + { + entity.EventArgs = await mwNode.ValueRef.ProcessAsync(entity); + + switch (entity.EventArgs.Routine) + { + //move next if continue is returned + case FpRoutine.Continue: + break; + + //Middleware completed the connection, time to exit the event processing + default: + return false; + } + + mwNode = mwNode.Next; + } + + return true; + } + + public void PostProcess(HttpEntity entity) + { + /* + * Middleware nodes may be allowed to inspect, or modify the return + * event arguments as the server may not have responded to the client + * yet. + */ + + LinkedListNode? mwNode = _chain.GetCurrentHead(); + + while (mwNode != null) + { + //Invoke mw handler on our event + mwNode.ValueRef.VolatilePostProcess(entity, ref entity.EventArgs); + + mwNode = mwNode.Next; + } + } + } +} diff --git a/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs b/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs index e65c26d..b60c7c3 100644 --- a/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs +++ b/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs @@ -90,40 +90,40 @@ namespace VNLib.Plugins.Essentials.Oauth ForwardOnlyWriter writer = new(buffer.Span); //Build the error message string - writer.Append("{\"error\":\""); + writer.AppendSmall("{\"error\":\""); switch (error) { case ErrorType.InvalidRequest: - writer.Append("invalid_request"); + writer.AppendSmall("invalid_request"); break; case ErrorType.InvalidClient: - writer.Append("invalid_client"); + writer.AppendSmall("invalid_client"); break; case ErrorType.UnauthorizedClient: - writer.Append("unauthorized_client"); + writer.AppendSmall("unauthorized_client"); break; case ErrorType.InvalidToken: - writer.Append("invalid_token"); + writer.AppendSmall("invalid_token"); break; case ErrorType.UnsupportedResponseType: - writer.Append("unsupported_response_type"); + writer.AppendSmall("unsupported_response_type"); break; case ErrorType.InvalidScope: - writer.Append("invalid_scope"); + writer.AppendSmall("invalid_scope"); break; case ErrorType.ServerError: - writer.Append("server_error"); + writer.AppendSmall("server_error"); break; case ErrorType.TemporarilyUnavailable: - writer.Append("temporarily_unavailable"); + writer.AppendSmall("temporarily_unavailable"); break; default: - writer.Append("error"); + writer.AppendSmall("error"); break; } - writer.Append("\",\"error_description\":\""); + writer.AppendSmall("\",\"error_description\":\""); writer.Append(description); - writer.Append("\"}"); + writer.AppendSmall("\"}"); //Close the response with the json data ev.CloseResponse(code, ContentType.Json, writer.AsSpan()); diff --git a/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs b/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs deleted file mode 100644 index e1706f4..0000000 --- a/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs +++ /dev/null @@ -1,188 +0,0 @@ -/* -* Copyright (c) 2024 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.Frozen; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -using VNLib.Net.Http; -using VNLib.Plugins.Essentials.Endpoints; - -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. - */ - - /// - /// A "lookup table" that represents virtual endpoints to be processed when an - /// incomming connection matches its path parameter - /// - private FrozenDictionary> VirtualEndpoints = - new Dictionary>(StringComparer.OrdinalIgnoreCase) - .ToFrozenDictionary(); - - private bool _isEmpty = true; - - /* - * A lock that is held by callers that intend to - * modify the vep table at the same time - */ - private readonly object VeUpdateLock = new(); - - /// - public bool IsEmpty => _isEmpty; - - - /// - public void AddEndpoint(params IEndpoint[] endpoints) - { - //Check - ArgumentNullException.ThrowIfNull(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> eps = endpoints - .Where(static e => e is IVirtualEndpoint) - .Select(static e => (IVirtualEndpoint)e); - - //Get http event endpoints and create wrapper classes for conversion - IEnumerable> evs = endpoints - .Where(static e => e is IVirtualEndpoint) - .Select(static e => new EvEndpointWrapper((e as IVirtualEndpoint)!)); - - //Uinion endpoints by their paths to combine them - IEnumerable> allEndpoints = eps.UnionBy(evs, static s => s.Path); - - //Only allow 1 thread at a time to mutate the table - lock (VeUpdateLock) - { - //Clone the current dictonary - Dictionary> newTable = new(VirtualEndpoints, StringComparer.OrdinalIgnoreCase); - //Insert the new eps, and/or overwrite old eps - foreach (IVirtualEndpoint ep in allEndpoints) - { - newTable.Add(ep.Path, ep); - } - - //Update is-empty flag - _isEmpty = newTable.Count == 0; - - //Create the new table and store the entire table - _ = Interlocked.Exchange(ref VirtualEndpoints, newTable.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase)); - } - } - - /// - public void RemoveEndpoint(params IEndpoint[] eps) - { - ArgumentNullException.ThrowIfNull(eps); - //Call remove on path - RemoveEndpoint(eps.Select(static s => s.Path).ToArray()); - } - - /// - public void RemoveEndpoint(params string[] paths) - { - ArgumentNullException.ThrowIfNull(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> newTable = new(VirtualEndpoints, StringComparer.OrdinalIgnoreCase); - - foreach (string eps in paths) - { - _ = newTable.Remove(eps); - } - - //Update is-empty flag - _isEmpty = newTable.Count == 0; - - //Store the new table - _ = Interlocked.Exchange(ref VirtualEndpoints, newTable.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase)); - } - } - - /// - public bool TryGetEndpoint(string path, [NotNullWhen(true)] out IVirtualEndpoint? endpoint) - => VirtualEndpoints.TryGetValue(path, out endpoint); - - - /* - * Wrapper class for converting IHttpEvent endpoints to - * httpEntityEndpoints - */ - private sealed class EvEndpointWrapper(IVirtualEndpoint Wrapped) : IVirtualEndpoint - { - string IEndpoint.Path => Wrapped.Path; - - ValueTask IVirtualEndpoint.Process(HttpEntity entity) => Wrapped.Process(entity); - } - } -} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj b/lib/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj index 4da640a..3a21ac9 100644 --- a/lib/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj +++ b/lib/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj @@ -2,13 +2,15 @@ net8.0 + enable VNLib.Plugins.Essentials VNLib.Plugins.Essentials - latest-all - True True false - enable + + + + latest-all -- cgit