aboutsummaryrefslogtreecommitdiff
path: root/lib/Plugins.Essentials/src
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-02-14 14:10:27 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2024-02-14 14:10:27 -0500
commit2b1314c1475e7e1831c691cf349cb89c66fa320c (patch)
tree091fc132a2bee2e79a68d8c6d5eb20f1d989a3d2 /lib/Plugins.Essentials/src
parentf4e4db7c5320976406feb252ae8f8bdbe9b3e351 (diff)
Squashed commit of the following:
commit ddd8a651b6eb43cfdd49d84056f8b9c34b543992 Author: vnugent <public@vaughnnugent.com> Date: Wed Feb 14 00:15:50 2024 -0500 ci: reduce output noise and update Argon2 build commit cf942959ff2feea03d3eda2ff2a263bdac4d6bc6 Author: vnugent <public@vaughnnugent.com> Date: Mon Feb 12 18:39:18 2024 -0500 chore: update packages and minor fixes commit ab506af9e2de2876b11bb45b3c7e787616c80155 Author: vnugent <public@vaughnnugent.com> Date: Fri Feb 9 21:27:24 2024 -0500 fix: patch and update core runtime service injection commit 7ed5e8b19164c28d3a238bd56878d2161fbea2e4 Author: vnugent <public@vaughnnugent.com> Date: Thu Feb 8 18:26:11 2024 -0500 fork dotnetplugins and make some intial updates/upgrades commit f4cab88d67be5da0953b14bd46fc972d4acc8606 Author: vnugent <public@vaughnnugent.com> Date: Thu Feb 8 12:16:13 2024 -0500 update some heap api functions commit 6035bf7ed8412f1da361cc5feddd860abfaf4fc1 Author: vnugent <public@vaughnnugent.com> Date: Wed Feb 7 22:09:11 2024 -0500 working file-watcher notifications/rework commit 698f8edf694ad9700ee2ce2220e692b496448ff9 Author: vnugent <public@vaughnnugent.com> Date: Wed Feb 7 20:37:28 2024 -0500 remove mem-template and add file-watcher utility commit b17591e0fb363222fcd7d93c2bad4ab1b102385f Author: vnugent <public@vaughnnugent.com> Date: Wed Feb 7 18:28:21 2024 -0500 add small memmove support for known small blocks commit 631be4d4b27fdbcd4b0526e17a128bb0d86911eb Author: vnugent <public@vaughnnugent.com> Date: Wed Feb 7 18:08:02 2024 -0500 setup some readonly ref arguments and convert copy apis to readonly refs commit 2ba8dec68d5cb192e61ad0141d4b460076d3f90a Author: vnugent <public@vaughnnugent.com> Date: Mon Feb 5 18:30:38 2024 -0500 restructure internal memmove strategies commit 25cf02872da980893ad7fb51d4eccc932380582b Author: vnugent <public@vaughnnugent.com> Date: Sun Feb 4 01:29:18 2024 -0500 add http stream interface, profiling -> file read updates commit 757668c44e78864dc69d5713a2cfba6db2ed9a2a Author: vnugent <public@vaughnnugent.com> Date: Fri Feb 2 14:27:04 2024 -0500 streamline data-copy api with proper large block support and net8 feature updates commit f22c1765fd72ab40a10d8ec92a8cb6d9ec1b1a04 Author: vnugent <public@vaughnnugent.com> Date: Mon Jan 29 16:16:23 2024 -0500 check for compression lib updates to close #2 and fix some ci build stuff commit f974bfdef6a795b4a1c04602502ef506ef2587a9 Author: vnugent <public@vaughnnugent.com> Date: Tue Jan 23 17:36:17 2024 -0500 switch allocator libs to lgpl2.1 commit 1fe5e01b329cd27b675000f1a557b784d3c88b56 Author: vnugent <public@vaughnnugent.com> Date: Tue Jan 23 17:05:59 2024 -0500 consolidate allocator packages and close #1 commit 74e1107e522f00b670526193396217f40a6bade7 Author: vnugent <public@vaughnnugent.com> Date: Tue Jan 23 15:43:40 2024 -0500 cache extension api tweaks commit 96ca2b0388a6326b9bb74f3ab2f62eaede6681e0 Author: vnugent <public@vaughnnugent.com> Date: Mon Jan 22 17:54:23 2024 -0500 explicit tcp server args reuse
Diffstat (limited to 'lib/Plugins.Essentials/src')
-rw-r--r--lib/Plugins.Essentials/src/Accounts/AccountUtils.cs43
-rw-r--r--lib/Plugins.Essentials/src/Content/DirectFileStream.cs120
-rw-r--r--lib/Plugins.Essentials/src/EventProcessor.cs386
-rw-r--r--lib/Plugins.Essentials/src/EventProcessorConfig.cs93
-rw-r--r--lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs18
-rw-r--r--lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs4
-rw-r--r--lib/Plugins.Essentials/src/HttpEntity.cs43
-rw-r--r--lib/Plugins.Essentials/src/IWebProcessorInfo.cs14
-rw-r--r--lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.cs4
-rw-r--r--lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs13
-rw-r--r--lib/Plugins.Essentials/src/Users/IUserManager.cs4
11 files changed, 521 insertions, 221 deletions
diff --git a/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs b/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs
index 396d496..38c62e0 100644
--- a/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs
+++ b/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -26,7 +26,6 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using System.Security.Cryptography;
-using System.Text.RegularExpressions;
using System.Runtime.CompilerServices;
using VNLib.Hashing;
@@ -81,12 +80,6 @@ namespace VNLib.Plugins.Essentials.Accounts
public const ulong MINIMUM_LEVEL = 0x0000000100000001L;
-
- /// <summary>
- /// Speical character regual expresion for basic checks
- /// </summary>
- public static readonly Regex SpecialCharacters = new(@"[\r\n\t\a\b\e\f#?!@$%^&*\+\-\~`|<>\{}]", RegexOptions.Compiled);
-
#region Password/User helper extensions
/// <summary>
@@ -101,8 +94,10 @@ namespace VNLib.Plugins.Essentials.Accounts
/// <returns>A value greater than 0 if successful, 0 or negative values if a failure occured</returns>
public static async Task<ERRNO> ValidatePasswordAsync(this IUserManager manager, IUser user, string password, PassValidateFlags flags, CancellationToken cancellation)
{
- _ = manager ?? throw new ArgumentNullException(nameof(manager));
- using PrivateString ps = new(password, false);
+ ArgumentNullException.ThrowIfNull(user);
+ ArgumentNullException.ThrowIfNull(manager);
+
+ using PrivateString ps = PrivateString.ToPrivateString(password, false);
return await manager.ValidatePasswordAsync(user, ps, flags, cancellation).ConfigureAwait(false);
}
@@ -118,8 +113,10 @@ namespace VNLib.Plugins.Essentials.Accounts
/// <returns>The result of the operation, the result should be 1 (aka true)</returns>
public static async Task<ERRNO> UpdatePasswordAsync(this IUserManager manager, IUser user, string password, CancellationToken cancellation = default)
{
- _ = manager ?? throw new ArgumentNullException(nameof(manager));
- using PrivateString ps = new(password, false);
+ ArgumentNullException.ThrowIfNull(user);
+ ArgumentNullException.ThrowIfNull(manager);
+
+ using PrivateString ps = PrivateString.ToPrivateString(password, false);
return await manager.UpdatePasswordAsync(user, ps, cancellation).ConfigureAwait(false);
}
@@ -140,6 +137,7 @@ namespace VNLib.Plugins.Essentials.Accounts
/// <returns>The origin of the account</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetAccountOrigin(this IUser ud) => ud[ACC_ORIGIN_ENTRY];
+
/// <summary>
/// If this account was created by any means other than a local account creation.
/// Implementors can use this method to specify the origin of the account. This field is not required
@@ -158,7 +156,7 @@ namespace VNLib.Plugins.Essentials.Accounts
/// <returns>A <see cref="PrivateString"/> that contains the new password hash</returns>
public static PrivateString GetRandomPassword(this IPasswordHashingProvider hashing, int size = RANDOM_PASS_SIZE)
{
- _ = hashing ?? throw new ArgumentNullException(nameof(hashing));
+ ArgumentNullException.ThrowIfNull(hashing);
//Get random bytes
using UnsafeMemoryHandle<byte> randBuffer = MemoryUtil.UnsafeAlloc(size);
@@ -175,7 +173,7 @@ namespace VNLib.Plugins.Essentials.Accounts
finally
{
//Zero the block and return to pool
- MemoryUtil.InitializeBlock(randBuffer.Span);
+ MemoryUtil.InitializeBlock(ref randBuffer.GetReference(), size);
}
}
@@ -190,10 +188,10 @@ namespace VNLib.Plugins.Essentials.Accounts
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Verify(this IPasswordHashingProvider provider, PrivateString passHash, PrivateString password)
{
- _ = provider ?? throw new ArgumentNullException(nameof(provider));
- _ = password ?? throw new ArgumentNullException(nameof(password));
- _ = passHash ?? throw new ArgumentNullException(nameof(passHash));
-
+ ArgumentNullException.ThrowIfNull(provider);
+ ArgumentNullException.ThrowIfNull(passHash);
+ ArgumentNullException.ThrowIfNull(password);
+
//Casting PrivateStrings to spans will reference the base string directly
return provider.Verify(passHash.ToReadOnlySpan(), password.ToReadOnlySpan());
}
@@ -208,8 +206,8 @@ namespace VNLib.Plugins.Essentials.Accounts
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PrivateString Hash(this IPasswordHashingProvider provider, PrivateString password)
{
- _ = provider ?? throw new ArgumentNullException(nameof(provider));
- _ = password ?? throw new ArgumentNullException(nameof(password));
+ ArgumentNullException.ThrowIfNull(provider);
+ ArgumentNullException.ThrowIfNull(password);
return provider.Hash(password.ToReadOnlySpan());
}
@@ -255,7 +253,8 @@ namespace VNLib.Plugins.Essentials.Accounts
/// <exception cref="InvalidOperationException"></exception>
public static IClientAuthorization GenerateAuthorization(this HttpEntity entity, IClientSecInfo secInfo, IUser user)
{
- _ = secInfo ?? throw new ArgumentNullException(nameof(secInfo));
+ ArgumentNullException.ThrowIfNull(user);
+ ArgumentNullException.ThrowIfNull(secInfo);
if (!entity.Session.IsSet || entity.Session.SessionType != SessionType.Web)
{
@@ -383,7 +382,7 @@ namespace VNLib.Plugins.Essentials.Accounts
/// <exception cref="NotSupportedException"></exception>
public static ERRNO TryEncryptClientData(this HttpEntity entity, IClientSecInfo secInfo, ReadOnlySpan<byte> data, Span<byte> output)
{
- _ = secInfo ?? throw new ArgumentNullException(nameof(secInfo));
+ ArgumentNullException.ThrowIfNull(secInfo);
//Use the default sec provider
IAccountSecurityProvider prov = entity.GetSecProviderOrThrow();
diff --git a/lib/Plugins.Essentials/src/Content/DirectFileStream.cs b/lib/Plugins.Essentials/src/Content/DirectFileStream.cs
new file mode 100644
index 0000000..67a6524
--- /dev/null
+++ b/lib/Plugins.Essentials/src/Content/DirectFileStream.cs
@@ -0,0 +1,120 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: DirectFileStream.cs
+*
+* DirectFileStream.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.IO;
+using System.Threading.Tasks;
+
+using Microsoft.Win32.SafeHandles;
+
+using VNLib.Utils;
+using VNLib.Net.Http;
+
+#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
+
+namespace VNLib.Plugins.Essentials.Content
+{
+
+ /*
+ * Why does this file exist? Well, in .NET streaming files is slow as crap.
+ * It is by-far the largest bottleneck in this framework.
+ *
+ * I wanted more direct control over file access for future file performance
+ * improvments. For now using the RandomAccess class bypasses the internal
+ * buffering used by the filestream class. I saw almost no difference in
+ * per-request performance but a slight reduction in processor usage across
+ * profiling sessions. This class also makes use of the new IHttpStreamResponse
+ * interface.
+ */
+
+ internal sealed class DirectFileStream(SafeFileHandle fileHandle) : VnDisposeable, IHttpStreamResponse
+ {
+ private long _position;
+
+ /// <summary>
+ /// Gets the current file pointer position
+ /// </summary>
+ public long Position => _position;
+
+ /// <summary>
+ /// Gets the length of the file
+ /// </summary>
+ public readonly long Length = RandomAccess.GetLength(fileHandle);
+
+ ///<inheritdoc/>
+ public async ValueTask<int> ReadAsync(Memory<byte> buffer)
+ {
+ //Read data from the file into the buffer, using the current position as the starting offset
+ long read = await RandomAccess.ReadAsync(fileHandle, buffer, _position, default);
+
+ _position += read;
+
+ return (int)read;
+ }
+
+ ///<inheritdoc/>
+ public ValueTask DisposeAsync()
+ {
+ //Interal dispose
+ Dispose();
+ return ValueTask.CompletedTask;
+ }
+
+ ///<inheritdoc/>
+ protected override void Free() => fileHandle.Dispose();
+
+ /// <summary>
+ /// Equivalent to <see cref="FileStream.Seek(long, SeekOrigin)"/> but for a
+ /// <see cref="DirectFileStream"/>
+ /// </summary>
+ /// <param name="offset">The number in bytes to see the stream position to</param>
+ /// <param name="origin">The offset origin</param>
+ public void Seek(long offset, SeekOrigin origin)
+ {
+ switch (origin)
+ {
+ case SeekOrigin.Begin:
+ ArgumentOutOfRangeException.ThrowIfNegative(offset);
+ _position = offset;
+ break;
+ case SeekOrigin.Current:
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(_position + offset, Length);
+ _position += offset;
+ break;
+ case SeekOrigin.End:
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(offset, 0);
+ _position = Length + offset;
+ break;
+ }
+ }
+
+ /// <summary>
+ /// Opens a file for direct access with default options
+ /// </summary>
+ /// <param name="fileName">The name of the file to open</param>
+ /// <returns>The new direct file-stream</returns>
+ public static DirectFileStream Open(string fileName)
+ => new(File.OpenHandle(fileName, options: FileOptions.SequentialScan | FileOptions.Asynchronous));
+ }
+} \ No newline at end of file
diff --git a/lib/Plugins.Essentials/src/EventProcessor.cs b/lib/Plugins.Essentials/src/EventProcessor.cs
index 861f318..61a9b64 100644
--- a/lib/Plugins.Essentials/src/EventProcessor.cs
+++ b/lib/Plugins.Essentials/src/EventProcessor.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -29,6 +29,8 @@ using System.Threading;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Runtime.CompilerServices;
using VNLib.Net.Http;
using VNLib.Utils.IO;
@@ -51,7 +53,7 @@ namespace VNLib.Plugins.Essentials
/// that breaks down simple processing procedures, routing, and session
/// loading.
/// </summary>
- public abstract class EventProcessor : IWebRoot, IWebProcessor
+ public abstract class EventProcessor(EventProcessorConfig config) : IWebRoot, IWebProcessor
{
private static readonly AsyncLocal<EventProcessor?> _currentProcessor = new();
@@ -61,24 +63,6 @@ namespace VNLib.Plugins.Essentials
public static EventProcessor? Current => _currentProcessor.Value;
/// <summary>
- /// The filesystem entrypoint path for the site
- /// </summary>
- public abstract string Directory { get; }
-
- ///<inheritdoc/>
- public abstract string Hostname { get; }
-
- /// <summary>
- /// Gets the EP processing options
- /// </summary>
- public abstract IEpProcessingOptions Options { get; }
-
- /// <summary>
- /// Event log provider
- /// </summary>
- protected abstract ILogProvider Log { get; }
-
- /// <summary>
/// <para>
/// Called when the server intends to process a file and requires translation from a
/// uri path to a usable filesystem path
@@ -125,73 +109,81 @@ namespace VNLib.Plugins.Essentials
public abstract void PostProcessEntity(HttpEntity entity, ref FileProcessArgs chosenRoutine);
///<inheritdoc/>
- public abstract IAccountSecurityProvider AccountSecurity { get; }
-
- /// <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();
-
-
- /// <summary>
- /// An <see cref="ISessionProvider"/> that connects stateful sessions to
- /// HTTP connections
- /// </summary>
- private ISessionProvider? Sessions;
+ public virtual EventProcessorConfig Options => config;
+ ///<inheritdoc/>
+ public string Hostname => config.Hostname;
+
+
+ /*
+ * Okay. So this is suposed to be a stupid fast lookup table for lock-free
+ * service pool exchanges. The goal is for future runtime service expansion.
+ *
+ * The reason lookups must be unnoticabyly fast is because the should be
+ * VERY rarley changed and will be read on every request.
+ *
+ * The goal of this table specifially is to make sure requesting a desired
+ * service is extremely fast and does not require any locks or synchronization.
+ */
+ const int SESS_INDEX = 0;
+ const int ROUTER_INDEX = 1;
+ const int SEC_INDEX = 2;
+
/// <summary>
- /// Sets or resets the current <see cref="ISessionProvider"/>
- /// for all connections
+ /// The internal service pool for the processor
/// </summary>
- /// <param name="sp">The new <see cref="ISessionProvider"/></param>
- public void SetSessionProvider(ISessionProvider? sp) => _ = Interlocked.Exchange(ref Sessions, sp);
+ protected readonly HttpProcessorServicePool ServicePool = new([
+ typeof(ISessionProvider), //Order must match the indexes above
+ typeof(IPageRouter),
+ typeof(IAccountSecurityProvider)
+ ]);
+
+ /*
+ * Fields are not marked as volatile because they should not
+ * really be updated at all in production uses, and if hot-reload
+ * is used, I don't consider a dirty read to be a large enough
+ * problem here.
+ */
- /// <summary>
- /// An <see cref="IPageRouter"/> to route files to be processed
- /// </summary>
- private IPageRouter? Router;
-
- /// <summary>
- /// Sets or resets the current <see cref="IPageRouter"/>
- /// for all connections
- /// </summary>
- /// <param name="router"><see cref="IPageRouter"/> to route incomming connections</param>
- public void SetPageRouter(IPageRouter? router) => _ = Interlocked.Exchange(ref Router, router);
+ private IAccountSecurityProvider? _accountSec;
+ private ISessionProvider? _sessions;
+ private IPageRouter? _router;
+
+ ///<inheritdoc/>
+ public IAccountSecurityProvider? AccountSecurity
+ {
+ //Exchange the version of the account security provider
+ get => ServicePool.ExchangeVersion(ref _accountSec, SEC_INDEX);
+ }
///<inheritdoc/>
public virtual async ValueTask ClientConnectedAsync(IHttpEvent httpEvent)
{
- //read local ref to session provider and page router
- ISessionProvider? _sessions = Sessions;
- IPageRouter? router = Router;
+ /*
+ * read any "volatile" properties into local copies for the duration
+ * of the request processing. This is to ensure that the properties
+ * are not changed during the processing of the request.
+ */
+
+ ISessionProvider? sessions = ServicePool.ExchangeVersion(ref _sessions, SESS_INDEX);
+ IPageRouter? router = ServicePool.ExchangeVersion(ref _router, ROUTER_INDEX);
+
+ LinkedListNode<IHttpMiddleware>? mwNode = config.MiddlewareChain.GetCurrentHead();
//event cancellation token
HttpEntity entity = new(httpEvent, this);
-
- LinkedListNode<IHttpMiddleware>? mwNode;
//Set ambient processor context
- _currentProcessor.Value = this;
+ _currentProcessor.Value = this;
try
{
//If sessions are set, get a session for the current connection
- if (_sessions != null)
+ if (sessions != null)
{
//Get the session
- entity.EventSessionHandle = await _sessions.GetSessionAsync(httpEvent, entity.EventCancellation);
+ entity.EventSessionHandle = await sessions.GetSessionAsync(httpEvent, entity.EventCancellation);
//If the processor had an error recovering the session, return the result to the processor
if (entity.EventSessionHandle.EntityStatus != FileProcessArgs.Continue)
@@ -206,7 +198,6 @@ namespace VNLib.Plugins.Essentials
try
{
- //Pre-process entity
PreProcessEntity(entity, out entity.EventArgs);
//If preprocess returned a value, exit
@@ -215,9 +206,6 @@ namespace VNLib.Plugins.Essentials
goto RespondAndExit;
}
- //Handle middleware before file processing
- mwNode = MiddlewareChain.GetCurrentHead();
-
//Loop through nodes
while(mwNode != null)
{
@@ -236,12 +224,12 @@ namespace VNLib.Plugins.Essentials
}
mwNode = mwNode.Next;
- }
+ }
- if (!EndpointTable.IsEmpty)
+ if (!config.EndpointTable.IsEmpty)
{
//See if the virtual file is servicable
- if (EndpointTable.TryGetEndpoint(entity.Server.Path, out IVirtualEndpoint<HttpEntity>? vf))
+ if (config.EndpointTable.TryGetEndpoint(entity.Server.Path, out IVirtualEndpoint<HttpEntity>? vf))
{
//Invoke the page handler process method
VfReturnType rt = await vf.Process(entity);
@@ -283,7 +271,7 @@ namespace VNLib.Plugins.Essentials
}
catch (Exception ex)
{
- Log.Error(ex, "Exception raised while releasing the assocated session");
+ config.Log.Error(ex, "Exception raised while releasing the assocated session");
}
}
@@ -305,17 +293,17 @@ namespace VNLib.Plugins.Essentials
}
catch (ResourceUpdateFailedException ruf)
{
- Log.Warn(ruf);
+ config.Log.Warn(ruf);
CloseWithError(HttpStatusCode.ServiceUnavailable, httpEvent);
}
catch (SessionException se)
{
- Log.Warn(se, "An exception was raised while attempting to get or save a session");
+ config.Log.Warn(se, "An exception was raised while attempting to get or save a session");
CloseWithError(HttpStatusCode.ServiceUnavailable, httpEvent);
}
catch (OperationCanceledException oce)
{
- Log.Warn(oce, "Request execution time exceeded, connection terminated");
+ config.Log.Warn(oce, "Request execution time exceeded, connection terminated");
CloseWithError(HttpStatusCode.ServiceUnavailable, httpEvent);
}
catch (IOException ioe) when (ioe.InnerException is SocketException)
@@ -324,7 +312,7 @@ namespace VNLib.Plugins.Essentials
}
catch (Exception ex)
{
- Log.Warn(ex, "Unhandled exception during application code execution.");
+ config.Log.Warn(ex, "Unhandled exception during application code execution.");
//Invoke the root error handler
CloseWithError(HttpStatusCode.InternalServerError, httpEvent);
}
@@ -341,7 +329,7 @@ namespace VNLib.Plugins.Essentials
/// </summary>
/// <param name="entity">The entity to process the file for</param>
/// <param name="args">The selected <see cref="FileProcessArgs"/> to determine what file to process</param>
- protected virtual void ProcessFile(IHttpEvent entity, in FileProcessArgs args)
+ protected virtual void ProcessFile(IHttpEvent entity, ref readonly FileProcessArgs args)
{
try
{
@@ -414,7 +402,7 @@ namespace VNLib.Plugins.Essentials
//See if the last modifed header was set
DateTimeOffset? ifModifedSince = entity.Server.LastModified();
-
+
//If the header was set, check the date, if the file has been modified since, continue sending the file
if (ifModifedSince.HasValue && ifModifedSince.Value > fileLastModified)
{
@@ -425,109 +413,97 @@ 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))
+ if (!entity.Server.Accepts(fileType))
{
- //set last modified time as the files last write time
- entity.Server.LastModified(fileLastModified);
-
- //try to open the selected file for reading and allow sharing
- FileStream fs = new (filename, FileMode.Open, FileAccess.Read, FileShare.Read);
-
- long endOffset = checked((long)entity.Server.Range.End);
- long startOffset = checked((long)entity.Server.Range.Start);
-
- //Follows rfc7233 -> https://www.rfc-editor.org/rfc/rfc7233#section-1.2
- switch (entity.Server.Range.RangeType)
- {
- case HttpRangeType.FullRange:
- if (endOffset > fs.Length || endOffset - startOffset < 0)
- {
- //Set acceptable range size
- entity.Server.Headers[HttpResponseHeader.ContentRange] = $"bytes */{fs.Length}";
-
- //The start offset is greater than the file length, return range not satisfiable
- entity.CloseResponse(HttpStatusCode.RequestedRangeNotSatisfiable);
- }
- else
- {
- //Seek the stream to the specified start position
- fs.Seek(startOffset, SeekOrigin.Begin);
-
- //Set range header, by passing the actual full content size
- entity.SetContentRangeHeader(entity.Server.Range, fs.Length);
+ //Unacceptable
+ CloseWithError(HttpStatusCode.NotAcceptable, entity);
+ return;
+ }
- //Send the response, with actual response length (diff between stream length and position)
- entity.CloseResponse(HttpStatusCode.PartialContent, fileType, fs, endOffset - startOffset + 1);
- }
- break;
- case HttpRangeType.FromStart:
- if (startOffset > fs.Length)
- {
- //Set acceptable range size
- entity.Server.Headers[HttpResponseHeader.ContentRange] = $"bytes */{fs.Length}";
+ //set last modified time as the files last write time
+ entity.Server.LastModified(fileLastModified);
- //The start offset is greater than the file length, return range not satisfiable
- entity.CloseResponse(HttpStatusCode.RequestedRangeNotSatisfiable);
- }
- else
- {
- //Seek the stream to the specified start position
- fs.Seek(startOffset, SeekOrigin.Begin);
+ //Open the file handle directly, reading will always be sequentially read and async
+ DirectFileStream dfs = DirectFileStream.Open(filename);
- //Set range header, by passing the actual full content size
- entity.SetContentRangeHeader(entity.Server.Range, fs.Length);
+ long endOffset = checked((long)entity.Server.Range.End);
+ long startOffset = checked((long)entity.Server.Range.Start);
- //Send the response, with actual response length (diff between stream length and position)
- entity.CloseResponse(HttpStatusCode.PartialContent, fileType, fs, fs.Length - fs.Position);
- }
- break;
-
- case HttpRangeType.FromEnd:
- if (endOffset > fs.Length)
- {
- //Set acceptable range size
- entity.Server.Headers[HttpResponseHeader.ContentRange] = $"bytes */{fs.Length}";
+ //Follows rfc7233 -> https://www.rfc-editor.org/rfc/rfc7233#section-1.2
+ switch (entity.Server.Range.RangeType)
+ {
+ case HttpRangeType.FullRange:
+ if (endOffset > dfs.Length || endOffset - startOffset < 0)
+ {
+ //The start offset is greater than the file length, return range not satisfiable
+ entity.Server.Headers[HttpResponseHeader.ContentRange] = $"bytes */{dfs.Length}";
+ entity.CloseResponse(HttpStatusCode.RequestedRangeNotSatisfiable);
+ }
+ else
+ {
+ //Seek the stream to the specified start position
+ dfs.Seek(startOffset, SeekOrigin.Begin);
- //The end offset is greater than the file length, return range not satisfiable
- entity.CloseResponse(HttpStatusCode.RequestedRangeNotSatisfiable);
- }
- else
- {
- //Seek the stream to the specified end position, server auto range will handle the rest
- fs.Seek(-endOffset, SeekOrigin.End);
+ //Set range header, by passing the actual full content size
+ entity.SetContentRangeHeader(entity.Server.Range, dfs.Length);
- //Set range header, by passing the actual full content size
- entity.SetContentRangeHeader(entity.Server.Range, fs.Length);
+ //Send the response, with actual response length (diff between stream length and position)
+ entity.CloseResponse(HttpStatusCode.PartialContent, fileType, dfs, endOffset - startOffset + 1);
+ }
+ break;
+ case HttpRangeType.FromStart:
+ if (startOffset > dfs.Length)
+ {
+ //The start offset is greater than the file length, return range not satisfiable
+ entity.Server.Headers[HttpResponseHeader.ContentRange] = $"bytes */{dfs.Length}";
+ entity.CloseResponse(HttpStatusCode.RequestedRangeNotSatisfiable);
+ }
+ else
+ {
+ //Seek the stream to the specified start position
+ dfs.Seek(startOffset, SeekOrigin.Begin);
+
+ entity.SetContentRangeHeader(entity.Server.Range, dfs.Length);
+
+ entity.CloseResponse(HttpStatusCode.PartialContent, fileType, dfs, dfs.Length - dfs.Position);
+ }
+ break;
- //Send the response, with actual response length (diff between stream length and position)
- entity.CloseResponse(HttpStatusCode.PartialContent, fileType, fs, fs.Length - fs.Position);
- }
- break;
- //No range or invalid range (the server is supposed to ignore invalid ranges)
- default:
- //send the whole file
- entity.CloseResponse(HttpStatusCode.OK, fileType, fs, fs.Length);
- break;
- }
-
- }
- else
- {
- //Unacceptable
- CloseWithError(HttpStatusCode.NotAcceptable, entity);
+ case HttpRangeType.FromEnd:
+ if (endOffset > dfs.Length)
+ {
+ //The end offset is greater than the file length, return range not satisfiable
+ entity.Server.Headers[HttpResponseHeader.ContentRange] = $"bytes */{dfs.Length}";
+ entity.CloseResponse(HttpStatusCode.RequestedRangeNotSatisfiable);
+ }
+ else
+ {
+ //Seek the stream to the specified end position, server auto range will handle the rest
+ dfs.Seek(-endOffset, SeekOrigin.End);
+
+ entity.SetContentRangeHeader(entity.Server.Range, dfs.Length);
+
+ entity.CloseResponse(HttpStatusCode.PartialContent, fileType, dfs, dfs.Length - dfs.Position);
+ }
+ break;
+ //No range or invalid range (the server is supposed to ignore invalid ranges)
+ default:
+ //send the whole file
+ entity.CloseResponse(HttpStatusCode.OK, fileType, dfs, dfs.Length);
+ break;
}
}
catch (IOException ioe)
{
- Log.Information(ioe, "Unhandled exception during file opening.");
+ config.Log.Information(ioe, "Unhandled exception during file opening.");
CloseWithError(HttpStatusCode.Locked, entity);
return;
}
catch (Exception ex)
{
- Log.Error(ex, "Unhandled exception during file opening.");
+ config.Log.Error(ex, "Unhandled exception during file opening.");
//Invoke the root error handler
CloseWithError(HttpStatusCode.InternalServerError, entity);
return;
@@ -690,5 +666,83 @@ namespace VNLib.Plugins.Essentials
}
return false;
}
+
+ /// <summary>
+ /// A pool of services that an <see cref="EventProcessor"/> will use can be exchanged at runtime
+ /// </summary>
+ /// <param name="expectedTypes">An ordered array of desired types</param>
+ protected sealed class HttpProcessorServicePool(Type[] expectedTypes)
+ {
+ private readonly uint[] _serviceTable = new uint[expectedTypes.Length];
+ private readonly WeakReference<object?>[] _objects = CreateServiceArray(expectedTypes.Length);
+ private readonly ImmutableArray<Type> _types = [.. expectedTypes];
+
+ /// <summary>
+ /// Gets all of the desired types for the servicec pool
+ /// </summary>
+ public ImmutableArray<Type> Types => _types;
+
+ /// <summary>
+ /// Sets a desired service instance in the pool, or clears it
+ /// from the pool.
+ /// </summary>
+ /// <param name="service">The service type to publish</param>
+ /// <param name="instance">The service instance to store</param>
+ public void SetService(Type service, object? instance)
+ {
+ ArgumentNullException.ThrowIfNull(service);
+
+ //Make sure the instance is of the correct type
+ if(instance is not null && !service.IsInstanceOfType(instance))
+ {
+ throw new ArgumentException("The instance does not match the service type");
+ }
+
+ //If the service type is not desired, return
+ int index = Array.IndexOf(expectedTypes, service);
+ if (index != -1)
+ {
+ //Set the service as a new weak reference atomically
+ Volatile.Write(ref _objects[index], new(instance));
+
+ //Notify that the service has been updated
+ Interlocked.Exchange(ref _serviceTable[index], 1);
+ }
+ }
+
+ /// <summary>
+ /// Determines if a desired services has been modified within
+ /// the pool, if it has, the service will be exchanged for the
+ /// new service.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="instance">A reference to the internal instance to exhange</param>
+ /// <param name="tableIndex">The constant index for the service type</param>
+ /// <returns>The exchanged service instance</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ internal T? ExchangeVersion<T>(ref T? instance, int tableIndex) where T : class?
+ {
+ //Clear modified flag
+ if (Interlocked.Exchange(ref _serviceTable[tableIndex], 0) == 1)
+ {
+ //Atomic read on the reference instance
+ WeakReference<object?> wr = Volatile.Read(ref _objects[tableIndex]);
+
+ //Try to get the object instance
+ wr.TryGetTarget(out object? value);
+
+ instance = (T?)value;
+ }
+
+ return instance;
+ }
+
+ private static WeakReference<object?>[] CreateServiceArray(int size)
+ {
+ WeakReference<object?>[] arr = new WeakReference<object?>[size];
+ Array.Fill(arr, new (null));
+ return arr;
+ }
+ }
}
} \ No newline at end of file
diff --git a/lib/Plugins.Essentials/src/EventProcessorConfig.cs b/lib/Plugins.Essentials/src/EventProcessorConfig.cs
new file mode 100644
index 0000000..8f401ac
--- /dev/null
+++ b/lib/Plugins.Essentials/src/EventProcessorConfig.cs
@@ -0,0 +1,93 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: EventProcessorConfig.cs
+*
+* EventProcessorConfig.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.IO;
+using System.Net;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+
+using VNLib.Utils.Logging;
+using VNLib.Plugins.Essentials.Middleware;
+
+namespace VNLib.Plugins.Essentials
+{
+ /// <summary>
+ /// An immutable configuration object for the <see cref="EventProcessor"/> that services the
+ /// lifetieme of the processor.
+ /// </summary>
+ /// <param name="Directory"> The filesystem entrypoint path for the site</param>
+ /// <param name="Hostname">The hostname the server will listen for, and the hostname that will identify this root when a connection requests it</param>
+ /// <param name="Log">The application log provider for writing logging messages to</param>
+ /// <param name="Options">Gets the EP processing options</param>
+ public record class EventProcessorConfig(string Directory, string Hostname, ILogProvider Log, IEpProcessingOptions Options)
+ {
+ /// <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 IVirtualEndpointTable EndpointTable { get; init; } = 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 IHttpMiddlewareChain MiddlewareChain { get; init; } = new SemiConistentMiddlewareChain();
+
+ /// <summary>
+ /// The name of a default file to search for within a directory if no file is specified (index.html).
+ /// This array should be ordered.
+ /// </summary>
+ public IReadOnlyCollection<string> DefaultFiles { get; init; } = [];
+
+ /// <summary>
+ /// File extensions that are denied from being read from the filesystem
+ /// </summary>
+ public FrozenSet<string> ExcludedExtensions { get; init; } = FrozenSet<string>.Empty;
+
+ /// <summary>
+ /// File attributes that must be matched for the file to be accessed
+ /// </summary>
+ public FileAttributes AllowedAttributes { get; init; }
+
+ /// <summary>
+ /// Files that match any attribute flag set will be denied
+ /// </summary>
+ public FileAttributes DissallowedAttributes { get; init; }
+
+ /// <summary>
+ /// A table of known downstream servers/ports that can be trusted to proxy connections
+ /// </summary>
+ public FrozenSet<IPAddress> DownStreamServers { get; init; } = FrozenSet<IPAddress>.Empty;
+
+ /// <summary>
+ /// A <see cref="TimeSpan"/> for how long a connection may remain open before all operations are cancelled
+ /// </summary>
+ public TimeSpan ExecutionTimeout { get; init; } = TimeSpan.Zero;
+ }
+} \ No newline at end of file
diff --git a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
index 5c36465..92fae08 100644
--- a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
+++ b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -165,10 +165,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static void SetContentRangeHeader(this IHttpEvent entity, in HttpRange range, long length)
{
- if(length < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(length), "Length must be greater than or equal to zero");
- }
+ ArgumentOutOfRangeException.ThrowIfNegative(length);
ulong start;
ulong end;
@@ -216,6 +213,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <returns>true if the user-agent specified the cors security header</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsCors(this IConnectionInfo server) => "cors".Equals(server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
+
/// <summary>
/// Determines if the User-Agent specified "cross-site" in the Sec-Site header, OR
/// the connection spcified an origin header and the origin's host does not match the
@@ -228,6 +226,7 @@ namespace VNLib.Plugins.Essentials.Extensions
return "cross-site".Equals(server.Headers[SEC_HEADER_SITE], StringComparison.OrdinalIgnoreCase)
|| (server.Origin != null && !server.RequestUri.DnsSafeHost.Equals(server.Origin.DnsSafeHost, StringComparison.Ordinal));
}
+
/// <summary>
/// Is the connection user-agent created, or automatic
/// </summary>
@@ -235,12 +234,14 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <returns>true if sec-user header was set to "?1"</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsUserInvoked(this IConnectionInfo server) => "?1".Equals(server.Headers[SEC_HEADER_USER], StringComparison.OrdinalIgnoreCase);
+
/// <summary>
/// Was this request created from normal user navigation
/// </summary>
/// <returns>true if sec-mode set to "navigate"</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsNavigation(this IConnectionInfo server) => "navigate".Equals(server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
+
/// <summary>
/// Determines if the client specified "no-cache" for the cache control header, signalling they do not wish to cache the entity
/// </summary>
@@ -250,6 +251,7 @@ namespace VNLib.Plugins.Essentials.Extensions
string? cache_header = server.Headers[HttpRequestHeader.CacheControl];
return !string.IsNullOrWhiteSpace(cache_header) && cache_header.Contains("no-cache", StringComparison.OrdinalIgnoreCase);
}
+
/// <summary>
/// Sets the response cache headers to match the requested caching type. Does not check against request headers
/// </summary>
@@ -266,6 +268,7 @@ namespace VNLib.Plugins.Essentials.Extensions
//Set the cache hader string using the http helper class
server.Headers[HttpResponseHeader.CacheControl] = HttpHelpers.GetCacheString(type, maxAge);
}
+
/// <summary>
/// Sets the Cache-Control response header to <see cref="NO_CACHE_RESPONSE_HEADER_VALUE"/>
/// and the pragma response header to 'no-cache'
@@ -291,6 +294,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool EnpointPortsMatch(this IConnectionInfo server) => server.RequestUri.Port == server.LocalEndpoint.Port;
+
/// <summary>
/// Determines if the host of the current request URI matches the referer header host
/// </summary>
@@ -300,6 +304,7 @@ namespace VNLib.Plugins.Essentials.Extensions
{
return server.RequestUri.DnsSafeHost.Equals(server.Referer?.DnsSafeHost, StringComparison.OrdinalIgnoreCase);
}
+
/// <summary>
/// Expires a client's cookie
/// </summary>
@@ -314,6 +319,7 @@ namespace VNLib.Plugins.Essentials.Extensions
{
server.SetCookie(name, string.Empty, domain, path, TimeSpan.Zero, sameSite, false, secure);
}
+
/// <summary>
/// Sets a cookie with an infinite (session life-span)
/// </summary>
@@ -403,6 +409,7 @@ namespace VNLib.Plugins.Essentials.Extensions
//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);
}
+
/// <summary>
/// Determines if the current connection is the loopback/internal network adapter
/// </summary>
@@ -443,6 +450,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <returns>The real ip of the connection</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IPAddress GetTrustedIp(this IConnectionInfo server) => GetTrustedIp(server, server.IsBehindDownStreamServer());
+
/// <summary>
/// Gets the real IP address of the request if behind a trusted downstream server, otherwise returns the transport remote ip address
/// </summary>
diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
index 638b52a..f49af32 100644
--- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
+++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -782,7 +782,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <exception cref="PathTooLongException"></exception>
/// <exception cref="ArgumentNullException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static DirectoryInfo GetRootDir(this HttpEntity ev) => new(ev.RequestedRoot.Directory);
+ public static DirectoryInfo GetRootDir(this HttpEntity ev) => new(ev.RequestedRoot.Options.Directory);
/// <summary>
/// Returns the MIME string representation of the content type of the uploaded file.
diff --git a/lib/Plugins.Essentials/src/HttpEntity.cs b/lib/Plugins.Essentials/src/HttpEntity.cs
index f48198b..efdb2cd 100644
--- a/lib/Plugins.Essentials/src/HttpEntity.cs
+++ b/lib/Plugins.Essentials/src/HttpEntity.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
/// A container for an <see cref="HttpEvent"/> with its attached session.
/// This class cannot be inherited.
/// </summary>
- public sealed class HttpEntity : IHttpEvent
+ public sealed class HttpEntity : IHttpEvent, IDisposable
{
/// <summary>
@@ -59,7 +59,23 @@ namespace VNLib.Plugins.Essentials
private readonly CancellationTokenSource EventCts;
- public HttpEntity(IHttpEvent entity, IWebProcessor root)
+ /// <summary>
+ /// Creates a new <see cref="HttpEntity"/> instance with the optional
+ /// session handle. If the session handle is set, the session will be
+ /// attached to the entity
+ /// </summary>
+ /// <param name="evnt">The event to parse and wrap</param>
+ /// <param name="root">The processor the connection has originated from</param>
+ /// <param name="session">An optional session handle to attach to the entity</param>
+ public HttpEntity(IHttpEvent evnt, IWebProcessor root, ref readonly SessionHandle session)
+ :this(evnt, root)
+ {
+ //Assign optional session and attempt to attach it
+ EventSessionHandle = session;
+ AttachSession();
+ }
+
+ internal HttpEntity(IHttpEvent entity, IWebProcessor root)
{
Entity = entity;
RequestedRoot = root;
@@ -100,12 +116,9 @@ namespace VNLib.Plugins.Essentials
}
/// <summary>
- /// Internal call to cleanup any internal resources
+ /// Cleans up internal resources
/// </summary>
- internal void Dispose()
- {
- EventCts.Dispose();
- }
+ public void Dispose() => EventCts.Dispose();
/// <summary>
/// A token that has a scheduled timeout to signal the cancellation of the entity event
@@ -209,6 +222,20 @@ namespace VNLib.Plugins.Essentials
}
///<inheritdoc/>
+ ///<exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void CloseResponse(HttpStatusCode code, ContentType type, IHttpStreamResponse stream, long length)
+ {
+ //Verify content type matches
+ if (!Server.Accepts(type))
+ {
+ throw new ContentTypeUnacceptableException("The client does not accept the content type of the response");
+ }
+
+ Entity.CloseResponse(code, type, stream, length);
+ }
+
+ ///<inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetControlFlag(ulong mask) => Entity.SetControlFlag(mask);
diff --git a/lib/Plugins.Essentials/src/IWebProcessorInfo.cs b/lib/Plugins.Essentials/src/IWebProcessorInfo.cs
index ae920ea..a523296 100644
--- a/lib/Plugins.Essentials/src/IWebProcessorInfo.cs
+++ b/lib/Plugins.Essentials/src/IWebProcessorInfo.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -33,20 +33,14 @@ namespace VNLib.Plugins.Essentials
public interface IWebProcessor : IWebRoot
{
/// <summary>
- /// The filesystem entrypoint path for the site
- /// </summary>
- string Directory { get; }
-
- /// <summary>
/// Gets the EP processing options
/// </summary>
- IEpProcessingOptions Options { get; }
+ EventProcessorConfig Options { get; }
/// <summary>
- /// The shared <see cref="IAccountSecurityProvider"/> that provides
- /// user account security operations
+ /// Gets the account security provider
/// </summary>
- IAccountSecurityProvider AccountSecurity { get; }
+ IAccountSecurityProvider? AccountSecurity { get; }
/// <summary>
/// <para>
diff --git a/lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.cs b/lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.cs
index 0a05c70..ed6ce3b 100644
--- a/lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.cs
+++ b/lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.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.Middleware
/// Removes a middleware handler from the chain
/// </summary>
/// <param name="middleware">The middleware instance to remove</param>
- void RemoveMiddleware(IHttpMiddleware middleware);
+ void Remove(IHttpMiddleware middleware);
/// <summary>
/// Removes all middleware handlers from the chain
diff --git a/lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs b/lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs
index 5d0c472..d3bc69b 100644
--- a/lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs
+++ b/lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -22,6 +22,7 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
+using System.Threading;
using System.Reflection;
using System.Collections.Generic;
@@ -67,10 +68,14 @@ namespace VNLib.Plugins.Essentials.Middleware
}
///<inheritdoc/>
- public LinkedListNode<IHttpMiddleware>? GetCurrentHead() => _middlewares.First;
+ public LinkedListNode<IHttpMiddleware>? GetCurrentHead()
+ {
+ LinkedList<IHttpMiddleware> currentTable = Volatile.Read(ref _middlewares);
+ return currentTable.First;
+ }
///<inheritdoc/>
- public void RemoveMiddleware(IHttpMiddleware middleware)
+ public void Remove(IHttpMiddleware middleware)
{
lock (_middlewares)
{
@@ -81,7 +86,7 @@ namespace VNLib.Plugins.Essentials.Middleware
newTable.Remove(middleware);
//Replace the current table with the new one
- _middlewares = newTable;
+ Volatile.Write(ref _middlewares, newTable);
}
}
}
diff --git a/lib/Plugins.Essentials/src/Users/IUserManager.cs b/lib/Plugins.Essentials/src/Users/IUserManager.cs
index 400a5d0..7b70f53 100644
--- a/lib/Plugins.Essentials/src/Users/IUserManager.cs
+++ b/lib/Plugins.Essentials/src/Users/IUserManager.cs
@@ -70,11 +70,11 @@ namespace VNLib.Plugins.Essentials.Users
/// <summary>
/// Attempts to get a user object without their password from the database asynchronously
/// </summary>
- /// <param name="emailAddress">The user's email address</param>
+ /// <param name="username">The user's uinque username</param>
/// <param name="cancellationToken">A token to cancel the operation</param>
/// <returns>The user's <see cref="IUser"/> object, null if the user was not found</returns>
/// <exception cref="ArgumentNullException"></exception>
- Task<IUser?> GetUserFromEmailAsync(string emailAddress, CancellationToken cancellationToken = default);
+ Task<IUser?> GetUserFromUsernameAsync(string username, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new user account in the store as per the request. The user-id field is optional,