From 2b1314c1475e7e1831c691cf349cb89c66fa320c Mon Sep 17 00:00:00 2001 From: vnugent Date: Wed, 14 Feb 2024 14:10:27 -0500 Subject: Squashed commit of the following: commit ddd8a651b6eb43cfdd49d84056f8b9c34b543992 Author: vnugent Date: Wed Feb 14 00:15:50 2024 -0500 ci: reduce output noise and update Argon2 build commit cf942959ff2feea03d3eda2ff2a263bdac4d6bc6 Author: vnugent Date: Mon Feb 12 18:39:18 2024 -0500 chore: update packages and minor fixes commit ab506af9e2de2876b11bb45b3c7e787616c80155 Author: vnugent Date: Fri Feb 9 21:27:24 2024 -0500 fix: patch and update core runtime service injection commit 7ed5e8b19164c28d3a238bd56878d2161fbea2e4 Author: vnugent Date: Thu Feb 8 18:26:11 2024 -0500 fork dotnetplugins and make some intial updates/upgrades commit f4cab88d67be5da0953b14bd46fc972d4acc8606 Author: vnugent Date: Thu Feb 8 12:16:13 2024 -0500 update some heap api functions commit 6035bf7ed8412f1da361cc5feddd860abfaf4fc1 Author: vnugent Date: Wed Feb 7 22:09:11 2024 -0500 working file-watcher notifications/rework commit 698f8edf694ad9700ee2ce2220e692b496448ff9 Author: vnugent Date: Wed Feb 7 20:37:28 2024 -0500 remove mem-template and add file-watcher utility commit b17591e0fb363222fcd7d93c2bad4ab1b102385f Author: vnugent Date: Wed Feb 7 18:28:21 2024 -0500 add small memmove support for known small blocks commit 631be4d4b27fdbcd4b0526e17a128bb0d86911eb Author: vnugent 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 Date: Mon Feb 5 18:30:38 2024 -0500 restructure internal memmove strategies commit 25cf02872da980893ad7fb51d4eccc932380582b Author: vnugent Date: Sun Feb 4 01:29:18 2024 -0500 add http stream interface, profiling -> file read updates commit 757668c44e78864dc69d5713a2cfba6db2ed9a2a Author: vnugent 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 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 Date: Tue Jan 23 17:36:17 2024 -0500 switch allocator libs to lgpl2.1 commit 1fe5e01b329cd27b675000f1a557b784d3c88b56 Author: vnugent Date: Tue Jan 23 17:05:59 2024 -0500 consolidate allocator packages and close #1 commit 74e1107e522f00b670526193396217f40a6bade7 Author: vnugent Date: Tue Jan 23 15:43:40 2024 -0500 cache extension api tweaks commit 96ca2b0388a6326b9bb74f3ab2f62eaede6681e0 Author: vnugent Date: Mon Jan 22 17:54:23 2024 -0500 explicit tcp server args reuse --- .../src/Accounts/AccountUtils.cs | 43 ++- .../src/Content/DirectFileStream.cs | 120 +++++++ lib/Plugins.Essentials/src/EventProcessor.cs | 386 ++++++++++++--------- lib/Plugins.Essentials/src/EventProcessorConfig.cs | 93 +++++ .../src/Extensions/ConnectionInfoExtensions.cs | 18 +- .../src/Extensions/EssentialHttpEventExtensions.cs | 4 +- lib/Plugins.Essentials/src/HttpEntity.cs | 43 ++- lib/Plugins.Essentials/src/IWebProcessorInfo.cs | 14 +- .../src/Middleware/IHttpMiddlewareChain.cs | 4 +- .../src/Middleware/SemiConistentMiddlewareChain.cs | 13 +- lib/Plugins.Essentials/src/Users/IUserManager.cs | 4 +- 11 files changed, 521 insertions(+), 221 deletions(-) create mode 100644 lib/Plugins.Essentials/src/Content/DirectFileStream.cs create mode 100644 lib/Plugins.Essentials/src/EventProcessorConfig.cs (limited to 'lib/Plugins.Essentials/src') 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; - - /// - /// Speical character regual expresion for basic checks - /// - public static readonly Regex SpecialCharacters = new(@"[\r\n\t\a\b\e\f#?!@$%^&*\+\-\~`|<>\{}]", RegexOptions.Compiled); - #region Password/User helper extensions /// @@ -101,8 +94,10 @@ namespace VNLib.Plugins.Essentials.Accounts /// A value greater than 0 if successful, 0 or negative values if a failure occured public static async Task 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 /// The result of the operation, the result should be 1 (aka true) public static async Task 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 /// The origin of the account [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetAccountOrigin(this IUser ud) => ud[ACC_ORIGIN_ENTRY]; + /// /// 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 /// A that contains the new password hash 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 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 /// 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 /// public static ERRNO TryEncryptClientData(this HttpEntity entity, IClientSecInfo secInfo, ReadOnlySpan data, Span 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; + + /// + /// Gets the current file pointer position + /// + public long Position => _position; + + /// + /// Gets the length of the file + /// + public readonly long Length = RandomAccess.GetLength(fileHandle); + + /// + public async ValueTask ReadAsync(Memory 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; + } + + /// + public ValueTask DisposeAsync() + { + //Interal dispose + Dispose(); + return ValueTask.CompletedTask; + } + + /// + protected override void Free() => fileHandle.Dispose(); + + /// + /// Equivalent to but for a + /// + /// + /// The number in bytes to see the stream position to + /// The offset origin + 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; + } + } + + /// + /// Opens a file for direct access with default options + /// + /// The name of the file to open + /// The new direct file-stream + 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. /// - public abstract class EventProcessor : IWebRoot, IWebProcessor + public abstract class EventProcessor(EventProcessorConfig config) : IWebRoot, IWebProcessor { private static readonly AsyncLocal _currentProcessor = new(); @@ -60,24 +62,6 @@ namespace VNLib.Plugins.Essentials /// public static EventProcessor? Current => _currentProcessor.Value; - /// - /// The filesystem entrypoint path for the site - /// - public abstract string Directory { get; } - - /// - public abstract string Hostname { get; } - - /// - /// Gets the EP processing options - /// - public abstract IEpProcessingOptions Options { get; } - - /// - /// Event log provider - /// - protected abstract ILogProvider Log { get; } - /// /// /// Called when the server intends to process a file and requires translation from a @@ -125,73 +109,81 @@ namespace VNLib.Plugins.Essentials public abstract void PostProcessEntity(HttpEntity entity, ref FileProcessArgs chosenRoutine); /// - public abstract IAccountSecurityProvider AccountSecurity { get; } - - /// - /// The table of virtual endpoints that will be used to process requests - /// - /// - /// May be overriden to provide a custom endpoint table - /// - public virtual IVirtualEndpointTable EndpointTable { get; } = new SemiConsistentVeTable(); - - /// - /// The middleware chain that will be used to process requests - /// - /// - /// If derrieved, may be overriden to provide a custom middleware chain - /// - public virtual IHttpMiddlewareChain MiddlewareChain { get; } = new SemiConistentMiddlewareChain(); - - - /// - /// An that connects stateful sessions to - /// HTTP connections - /// - private ISessionProvider? Sessions; + public virtual EventProcessorConfig Options => config; + /// + 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; + /// - /// Sets or resets the current - /// for all connections + /// The internal service pool for the processor /// - /// The new - 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. + */ - /// - /// An to route files to be processed - /// - private IPageRouter? Router; - - /// - /// Sets or resets the current - /// for all connections - /// - /// to route incomming connections - public void SetPageRouter(IPageRouter? router) => _ = Interlocked.Exchange(ref Router, router); + private IAccountSecurityProvider? _accountSec; + private ISessionProvider? _sessions; + private IPageRouter? _router; + + /// + public IAccountSecurityProvider? AccountSecurity + { + //Exchange the version of the account security provider + get => ServicePool.ExchangeVersion(ref _accountSec, SEC_INDEX); + } /// 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? mwNode = config.MiddlewareChain.GetCurrentHead(); //event cancellation token HttpEntity entity = new(httpEvent, this); - - LinkedListNode? 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? vf)) + if (config.EndpointTable.TryGetEndpoint(entity.Server.Path, out IVirtualEndpoint? 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 /// /// The entity to process the file for /// The selected to determine what file to process - 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; } + + /// + /// A pool of services that an will use can be exchanged at runtime + /// + /// An ordered array of desired types + protected sealed class HttpProcessorServicePool(Type[] expectedTypes) + { + private readonly uint[] _serviceTable = new uint[expectedTypes.Length]; + private readonly WeakReference[] _objects = CreateServiceArray(expectedTypes.Length); + private readonly ImmutableArray _types = [.. expectedTypes]; + + /// + /// Gets all of the desired types for the servicec pool + /// + public ImmutableArray Types => _types; + + /// + /// Sets a desired service instance in the pool, or clears it + /// from the pool. + /// + /// The service type to publish + /// The service instance to store + 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); + } + } + + /// + /// Determines if a desired services has been modified within + /// the pool, if it has, the service will be exchanged for the + /// new service. + /// + /// + /// A reference to the internal instance to exhange + /// The constant index for the service type + /// The exchanged service instance + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal T? ExchangeVersion(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 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[] CreateServiceArray(int size) + { + WeakReference[] arr = new WeakReference[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 +{ + /// + /// An immutable configuration object for the that services the + /// lifetieme of the processor. + /// + /// The filesystem entrypoint path for the site + /// The hostname the server will listen for, and the hostname that will identify this root when a connection requests it + /// The application log provider for writing logging messages to + /// Gets the EP processing options + public record class EventProcessorConfig(string Directory, string Hostname, ILogProvider Log, IEpProcessingOptions Options) + { + /// + /// The table of virtual endpoints that will be used to process requests + /// + /// + /// May be overriden to provide a custom endpoint table + /// + public IVirtualEndpointTable EndpointTable { get; init; } = new SemiConsistentVeTable(); + + /// + /// The middleware chain that will be used to process requests + /// + /// + /// If derrieved, may be overriden to provide a custom middleware chain + /// + public IHttpMiddlewareChain MiddlewareChain { get; init; } = new SemiConistentMiddlewareChain(); + + /// + /// The name of a default file to search for within a directory if no file is specified (index.html). + /// This array should be ordered. + /// + public IReadOnlyCollection DefaultFiles { get; init; } = []; + + /// + /// File extensions that are denied from being read from the filesystem + /// + public FrozenSet ExcludedExtensions { get; init; } = FrozenSet.Empty; + + /// + /// File attributes that must be matched for the file to be accessed + /// + public FileAttributes AllowedAttributes { get; init; } + + /// + /// Files that match any attribute flag set will be denied + /// + public FileAttributes DissallowedAttributes { get; init; } + + /// + /// A table of known downstream servers/ports that can be trusted to proxy connections + /// + public FrozenSet DownStreamServers { get; init; } = FrozenSet.Empty; + + /// + /// A for how long a connection may remain open before all operations are cancelled + /// + 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 /// 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 /// 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); + /// /// 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)); } + /// /// Is the connection user-agent created, or automatic /// @@ -235,12 +234,14 @@ 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); + /// /// 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); + /// /// Determines if the client specified "no-cache" for the cache control header, signalling they do not wish to cache the entity /// @@ -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); } + /// /// Sets the response cache headers to match the requested caching type. Does not check against request headers /// @@ -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); } + /// /// Sets the Cache-Control response header to /// and the pragma response header to 'no-cache' @@ -291,6 +294,7 @@ namespace VNLib.Plugins.Essentials.Extensions /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EnpointPortsMatch(this IConnectionInfo server) => server.RequestUri.Port == server.LocalEndpoint.Port; + /// /// Determines if the host of the current request URI matches the referer header host /// @@ -300,6 +304,7 @@ namespace VNLib.Plugins.Essentials.Extensions { return server.RequestUri.DnsSafeHost.Equals(server.Referer?.DnsSafeHost, StringComparison.OrdinalIgnoreCase); } + /// /// Expires a client's cookie /// @@ -314,6 +319,7 @@ namespace VNLib.Plugins.Essentials.Extensions { server.SetCookie(name, string.Empty, domain, path, TimeSpan.Zero, sameSite, false, secure); } + /// /// Sets a cookie with an infinite (session life-span) /// @@ -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); } + /// /// Determines if the current connection is the loopback/internal network adapter /// @@ -443,6 +450,7 @@ namespace VNLib.Plugins.Essentials.Extensions /// The real ip of the connection [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IPAddress GetTrustedIp(this IConnectionInfo server) => GetTrustedIp(server, server.IsBehindDownStreamServer()); + /// /// Gets the real IP address of the request if behind a trusted downstream server, otherwise returns the transport remote ip address /// 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 /// /// [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); /// /// 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 with its attached session. /// This class cannot be inherited. /// - public sealed class HttpEntity : IHttpEvent + public sealed class HttpEntity : IHttpEvent, IDisposable { /// @@ -59,7 +59,23 @@ namespace VNLib.Plugins.Essentials private readonly CancellationTokenSource EventCts; - public HttpEntity(IHttpEvent entity, IWebProcessor root) + /// + /// Creates a new instance with the optional + /// session handle. If the session handle is set, the session will be + /// attached to the entity + /// + /// The event to parse and wrap + /// The processor the connection has originated from + /// An optional session handle to attach to the entity + 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 } /// - /// Internal call to cleanup any internal resources + /// Cleans up internal resources /// - internal void Dispose() - { - EventCts.Dispose(); - } + public void Dispose() => EventCts.Dispose(); /// /// A token that has a scheduled timeout to signal the cancellation of the entity event @@ -208,6 +221,20 @@ namespace VNLib.Plugins.Essentials Entity.CloseResponse(code, type, entity); } + /// + /// + [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); + } + /// [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 @@ -32,21 +32,15 @@ namespace VNLib.Plugins.Essentials /// public interface IWebProcessor : IWebRoot { - /// - /// The filesystem entrypoint path for the site - /// - string Directory { get; } - /// /// Gets the EP processing options /// - IEpProcessingOptions Options { get; } + EventProcessorConfig Options { get; } /// - /// The shared that provides - /// user account security operations + /// Gets the account security provider /// - IAccountSecurityProvider AccountSecurity { get; } + IAccountSecurityProvider? AccountSecurity { get; } /// /// 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 /// /// The middleware instance to remove - void RemoveMiddleware(IHttpMiddleware middleware); + void Remove(IHttpMiddleware middleware); /// /// 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 } /// - public LinkedListNode? GetCurrentHead() => _middlewares.First; + public LinkedListNode? GetCurrentHead() + { + LinkedList currentTable = Volatile.Read(ref _middlewares); + return currentTable.First; + } /// - 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 /// /// Attempts to get a user object without their password from the database asynchronously /// - /// The user's email address + /// The user's uinque username /// A token to cancel the operation /// The user's object, null if the user was not found /// - Task GetUserFromEmailAsync(string emailAddress, CancellationToken cancellationToken = default); + Task GetUserFromUsernameAsync(string username, CancellationToken cancellationToken = default); /// /// Creates a new user account in the store as per the request. The user-id field is optional, -- cgit