From b679ddd4e647ac915febd0d5a5e488a1e8e48842 Mon Sep 17 00:00:00 2001 From: vnugent Date: Thu, 29 Feb 2024 21:23:26 -0500 Subject: Squashed commit of the following: commit 231e26e5c6731e6e156d7c0591518e84a3b82f5a Author: vnugent Date: Thu Feb 29 20:59:42 2024 -0500 fix: #5 find and patch Windows compression bug and some deployment commit d0bfe14e0a0e27172b8dd41f468265e651784837 Author: vnugent Date: Wed Feb 21 21:39:19 2024 -0500 fix: #4 fix readme licensing message for accuracy commit 6f37f152fcd105e40af6689192a36b87eda95f51 Author: vnugent Date: Wed Feb 21 21:37:55 2024 -0500 fix: jwt hashalg sizing and public api for hashalg sizes --- .../src/Accounts/UserCreationRequest.cs | 16 +++- lib/Plugins.Essentials/src/EventProcessor.cs | 18 ++-- .../src/Extensions/ConnectionInfoExtensions.cs | 1 + .../src/Extensions/EssentialHttpEventExtensions.cs | 98 +++++++++++++++++----- .../src/SemiConsistentVeTable.cs | 25 ++++-- .../src/Sessions/ISessionExtensions.cs | 4 +- .../src/Users/IUserCreationRequest.cs | 4 +- 7 files changed, 120 insertions(+), 46 deletions(-) (limited to 'lib/Plugins.Essentials') diff --git a/lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs b/lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs index e346af1..2f2ba91 100644 --- a/lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs +++ b/lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -22,6 +22,8 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; + using VNLib.Utils.Memory; using VNLib.Plugins.Essentials.Users; @@ -40,7 +42,17 @@ namespace VNLib.Plugins.Essentials.Accounts public ulong Privileges { get; init; } = AccountUtil.MINIMUM_LEVEL; /// - public string EmailAddress { get; init; } = string.Empty; + public string Username { get; init; } = string.Empty; + + /// + /// Obsolete: Use the Username property instead + /// + [Obsolete("Use the Username property instead")] + public string EmailAddress + { + get => Username; + init => Username = value; + } /// public bool UseRawPassword { get; init; } diff --git a/lib/Plugins.Essentials/src/EventProcessor.cs b/lib/Plugins.Essentials/src/EventProcessor.cs index 61a9b64..f052c56 100644 --- a/lib/Plugins.Essentials/src/EventProcessor.cs +++ b/lib/Plugins.Essentials/src/EventProcessor.cs @@ -188,8 +188,7 @@ namespace VNLib.Plugins.Essentials //If the processor had an error recovering the session, return the result to the processor if (entity.EventSessionHandle.EntityStatus != FileProcessArgs.Continue) { - ProcessFile(httpEvent, entity.EventSessionHandle.EntityStatus); - return; + goto ProcessRoutine; } //Attach the new session to the entity @@ -209,7 +208,7 @@ namespace VNLib.Plugins.Essentials //Loop through nodes while(mwNode != null) { - //Process + //Invoke mw handler on our event entity.EventArgs = await mwNode.ValueRef.ProcessAsync(entity); switch (entity.EventArgs.Routine) @@ -237,7 +236,7 @@ namespace VNLib.Plugins.Essentials //Process a virtual file GetArgsFromVirtualReturn(entity, rt, out entity.EventArgs); - //If the entity was processed, exit + //If the entity was processed by the handler, exit if (entity.EventArgs != FileProcessArgs.Continue) { goto RespondAndExit; @@ -275,8 +274,10 @@ namespace VNLib.Plugins.Essentials } } + ProcessRoutine: + //Finally process the file - ProcessFile(httpEvent, in entity.EventArgs); + ProcessRoutine(httpEvent, in entity.EventArgs); } catch (ContentTypeUnacceptableException) { @@ -329,7 +330,7 @@ namespace VNLib.Plugins.Essentials /// /// The entity to process the file for /// The selected to determine what file to process - protected virtual void ProcessFile(IHttpEvent entity, ref readonly FileProcessArgs args) + protected virtual void ProcessRoutine(IHttpEvent entity, ref readonly FileProcessArgs args) { try { @@ -413,8 +414,7 @@ namespace VNLib.Plugins.Essentials //Get the content type of he file ContentType fileType = HttpHelpers.GetContentTypeFromFile(filename); - - //Make sure the client accepts the content type + if (!entity.Server.Accepts(fileType)) { //Unacceptable @@ -729,7 +729,7 @@ namespace VNLib.Plugins.Essentials WeakReference wr = Volatile.Read(ref _objects[tableIndex]); //Try to get the object instance - wr.TryGetTarget(out object? value); + _ = wr.TryGetTarget(out object? value); instance = (T?)value; } diff --git a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs index 92fae08..a99b1ab 100644 --- a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs +++ b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs @@ -354,6 +354,7 @@ namespace VNLib.Plugins.Essentials.Extensions /// /// /// + /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs index f49af32..5f59346 100644 --- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs +++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs @@ -163,6 +163,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponseJson(this IHttpEvent ev, HttpStatusCode code, JsonDocument data) { + ArgumentNullException.ThrowIfNull(ev); + if(data == null) { ev.CloseResponse(code); @@ -195,7 +197,9 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponse(this IHttpEvent ev, T webm) where T:WebMessage { - if (webm == null) + ArgumentNullException.ThrowIfNull(ev); + + if (webm is null) { ev.CloseResponse(HttpStatusCode.OK); } @@ -220,6 +224,9 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, FileInfo file) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(file); + //Close with file ev.CloseResponse(code, file); //Set content dispostion as attachment (only if successfull) @@ -236,6 +243,9 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, FileStream file) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(file); + //Close with file ev.CloseResponse(code, file); //Set content dispostion as attachment (only if successfull) @@ -255,6 +265,9 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, ContentType ct, Stream data, string fileName, long length) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(data); + //Close with file ev.CloseResponse(code, ct, data, length); //Set content dispostion as attachment (only if successfull) @@ -275,6 +288,9 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, FileInfo file) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(file); + //Open filestream for file FileStream fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read); try @@ -302,13 +318,16 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, FileStream file) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(file); + //Get content type from filename ContentType ct = HttpHelpers.GetContentTypeFromFile(file.Name); //Set the input as a stream ev.CloseResponse(code, ct, file, file.Length); } - + /// /// Close a response to a connection with a character buffer using the server wide /// encoding @@ -321,12 +340,10 @@ namespace VNLib.Plugins.Essentials.Extensions /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data) - { + public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data) => //Get a memory stream using server built-in encoding CloseResponse(ev, code, type, data, ev.Server.Encoding); - } - + /// /// Close a response to a connection with a character buffer using the specified encoding type /// @@ -339,6 +356,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data, Encoding encoding) { + ArgumentNullException.ThrowIfNull(ev); + if (data.IsEmpty) { ev.CloseResponse(code); @@ -367,6 +386,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data) { + ArgumentNullException.ThrowIfNull(ev); + if (data.IsEmpty) { ev.CloseResponse(code); @@ -391,6 +412,9 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool CloseWithRelativeFile(this HttpEntity entity, HttpStatusCode code, string filePath) { + ArgumentNullException.ThrowIfNull(entity); + ArgumentNullException.ThrowIfNull(filePath); + //See if file exists and is within the root's directory if (entity.RequestedRoot.FindResourceInRoot(filePath, out string realPath)) { @@ -412,11 +436,9 @@ namespace VNLib.Plugins.Essentials.Extensions /// Sets required headers for redirection, disables cache control, and returns the status code to the client /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Redirect(this IHttpEvent ev, RedirectType type, string location) - { - Redirect(ev, type, new Uri(location, UriKind.RelativeOrAbsolute)); - } - + public static void Redirect(this IHttpEvent ev, RedirectType type, string location) + => Redirect(ev, type, new Uri(location, UriKind.RelativeOrAbsolute)); + /// /// Redirects a client using the specified /// @@ -427,6 +449,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Redirect(this IHttpEvent ev, RedirectType type, Uri location) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(location); if(type == RedirectType.None) { @@ -472,6 +496,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryGetJsonFromArg(this IHttpEvent ev, string key, JsonSerializerOptions options, out T? obj) { + ArgumentNullException.ThrowIfNull(ev); + //Check for key in argument if (ev.RequestArgs.TryGetNonEmptyValue(key, out string? value)) { @@ -502,6 +528,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static JsonDocument? GetJsonFromArg(this IHttpEvent ev, string key, in JsonDocumentOptions options = default) { + ArgumentNullException.ThrowIfNull(ev); + try { //Check for key in argument @@ -528,6 +556,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T? GetJsonFromFile(this IHttpEvent ev, JsonSerializerOptions? options = null, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + if (ev.Files.Count <= uploadIndex) { return default; @@ -563,6 +593,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static JsonDocument? GetJsonFromFile(this IHttpEvent ev, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + if (ev.Files.Count <= uploadIndex) { return default; @@ -599,6 +631,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ValueTask GetJsonFromFileAsync(this HttpEntity ev, JsonSerializerOptions? options = null, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + if (ev.Files.Count <= uploadIndex) { return ValueTask.FromResult(default); @@ -640,6 +674,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Task GetJsonFromFileAsync(this HttpEntity ev, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + if (ev.Files.Count <= uploadIndex) { return DocTaskDefault; @@ -677,6 +713,9 @@ namespace VNLib.Plugins.Essentials.Extensions /// public static Task ParseFileAsAsync(this IHttpEvent ev, Func> parser, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(parser); + if (ev.Files.Count <= uploadIndex) { return Task.FromResult(default); @@ -698,6 +737,9 @@ namespace VNLib.Plugins.Essentials.Extensions /// public static Task ParseFileAsAsync(this IHttpEvent ev, Func> parser, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(parser); + if (ev.Files.Count <= uploadIndex) { return Task.FromResult(default); @@ -720,6 +762,9 @@ namespace VNLib.Plugins.Essentials.Extensions /// public static ValueTask ParseFileAsAsync(this IHttpEvent ev, Func> parser, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(parser); + if (ev.Files.Count <= uploadIndex) { return ValueTask.FromResult(default); @@ -741,6 +786,9 @@ namespace VNLib.Plugins.Essentials.Extensions /// public static ValueTask ParseFileAsAsync(this IHttpEvent ev, Func> parser, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(parser); + if (ev.Files.Count <= uploadIndex) { return ValueTask.FromResult(default); @@ -760,6 +808,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool HasAuthorization(this IConnectionInfo ci, [NotNullWhen(true)] out string? token) { + ArgumentNullException.ThrowIfNull(ci); + //Get auth header value string? authorization = ci.Headers[HttpRequestHeader.Authorization]; //Check if its set @@ -767,7 +817,7 @@ namespace VNLib.Plugins.Essentials.Extensions { int bearerIndex = authorization.IndexOf(BEARER_STRING, StringComparison.OrdinalIgnoreCase); //Calc token offset, get token, and trim any whitespace - token = authorization[(bearerIndex + BEARER_LEN)..].Trim(); + token = authorization.AsSpan(bearerIndex + BEARER_LEN).Trim().ToString(); return true; } token = null; @@ -819,11 +869,9 @@ namespace VNLib.Plugins.Essentials.Extensions ) { //Must define an accept callback - _ = socketOpenedCallback ?? throw new ArgumentNullException(nameof(socketOpenedCallback)); - - bool success = PrepWebSocket(entity, subProtocol); + ArgumentNullException.ThrowIfNull(socketOpenedCallback); - if (success) + if (PrepWebSocket(entity, subProtocol)) { //Set a default keep alive if none was specified if (keepAlive == default) @@ -841,8 +889,11 @@ namespace VNLib.Plugins.Essentials.Extensions //Setup a new websocket session with a new session id entity.DangerousChangeProtocol(ws); + + return true; } - return success; + + return false; } /// @@ -858,11 +909,10 @@ namespace VNLib.Plugins.Essentials.Extensions 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)); + ArgumentNullException.ThrowIfNull(entity); + ArgumentNullException.ThrowIfNull(socketOpenedCallback); - bool success = PrepWebSocket(entity, subProtocol); - - if(success) + if(PrepWebSocket(entity, subProtocol)) { //Set a default keep alive if none was specified if (keepAlive == default) @@ -879,15 +929,19 @@ namespace VNLib.Plugins.Essentials.Extensions //Setup a new websocket session with a new session id entity.DangerousChangeProtocol(ws); + + return true; } - return success; + return false; } private static string GetNewSocketId() => Guid.NewGuid().ToString("N"); private static bool PrepWebSocket(this IHttpEvent entity, string? subProtocol = null) { + ArgumentNullException.ThrowIfNull(entity); + //Make sure this is a websocket request if (!entity.Server.IsWebSocketRequest) { diff --git a/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs b/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs index a235b13..e1706f4 100644 --- a/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs +++ b/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs @@ -28,6 +28,7 @@ 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; @@ -65,6 +66,7 @@ namespace VNLib.Plugins.Essentials new Dictionary>(StringComparer.OrdinalIgnoreCase) .ToFrozenDictionary(); + private bool _isEmpty = true; /* * A lock that is held by callers that intend to @@ -73,14 +75,14 @@ namespace VNLib.Plugins.Essentials private readonly object VeUpdateLock = new(); /// - public bool IsEmpty => VirtualEndpoints.Count == 0; + public bool IsEmpty => _isEmpty; /// public void AddEndpoint(params IEndpoint[] endpoints) { //Check - _ = endpoints ?? throw new ArgumentNullException(nameof(endpoints)); + ArgumentNullException.ThrowIfNull(endpoints); //Make sure all endpoints specify a path if (endpoints.Any(static e => string.IsNullOrWhiteSpace(e?.Path))) { @@ -105,6 +107,7 @@ namespace VNLib.Plugins.Essentials //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 @@ -115,10 +118,11 @@ namespace VNLib.Plugins.Essentials newTable.Add(ep.Path, ep); } - FrozenDictionary> newTableFrozen = newTable.ToFrozenDictionary(); + //Update is-empty flag + _isEmpty = newTable.Count == 0; - //Store the new table - _ = Interlocked.Exchange(ref VirtualEndpoints, newTableFrozen); + //Create the new table and store the entire table + _ = Interlocked.Exchange(ref VirtualEndpoints, newTable.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase)); } } @@ -157,24 +161,27 @@ namespace VNLib.Plugins.Essentials _ = newTable.Remove(eps); } - FrozenDictionary> newTableFrozen = newTable.ToFrozenDictionary(); + //Update is-empty flag + _isEmpty = newTable.Count == 0; //Store the new table - _ = Interlocked.Exchange(ref VirtualEndpoints, newTableFrozen); + _ = Interlocked.Exchange(ref VirtualEndpoints, newTable.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase)); } } /// - public bool TryGetEndpoint(string path, out IVirtualEndpoint? endpoint) => VirtualEndpoints.TryGetValue(path, out endpoint); + 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 record class EvEndpointWrapper(IVirtualEndpoint Wrapped) : IVirtualEndpoint + private sealed class EvEndpointWrapper(IVirtualEndpoint Wrapped) : IVirtualEndpoint { string IEndpoint.Path => Wrapped.Path; + ValueTask IVirtualEndpoint.Process(HttpEntity entity) => Wrapped.Process(entity); } } diff --git a/lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs b/lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs index d3fc475..05d6712 100644 --- a/lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs +++ b/lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -49,7 +49,7 @@ namespace VNLib.Plugins.Essentials.Sessions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetOrigin(this ISession session) => session[ORIGIN_ENTRY]; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Uri GetOriginUri(this ISession session) => Uri.TryCreate(session[ORIGIN_ENTRY], UriKind.Absolute, out Uri origin) ? origin : null; + public static Uri? GetOriginUri(this ISession session) => Uri.TryCreate(session[ORIGIN_ENTRY], UriKind.Absolute, out Uri? origin) ? origin : null; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SetOrigin(this ISession session, string origin) => session[ORIGIN_ENTRY] = origin; diff --git a/lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs b/lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs index a5b9a30..bd89be1 100644 --- a/lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs +++ b/lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs @@ -44,9 +44,9 @@ namespace VNLib.Plugins.Essentials.Users ulong Privileges { get; } /// - /// The user's email address + /// The user's unique username (may also be an email address /// - string EmailAddress { get; } + string Username { get; } /// /// Should the password be stored as-is in the database? -- cgit