aboutsummaryrefslogtreecommitdiff
path: root/lib/Plugins.Essentials
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Plugins.Essentials')
-rw-r--r--lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs100
-rw-r--r--lib/Plugins.Essentials/src/Endpoints/SemiConsistentVeTable.cs (renamed from lib/Plugins.Essentials/src/SemiConsistentVeTable.cs)7
-rw-r--r--lib/Plugins.Essentials/src/EventProcessor.cs37
-rw-r--r--lib/Plugins.Essentials/src/EventProcessorConfig.cs1
-rw-r--r--lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs131
-rw-r--r--lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs64
-rw-r--r--lib/Plugins.Essentials/src/Extensions/HttpCookie.cs3
-rw-r--r--lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs18
-rw-r--r--lib/Plugins.Essentials/src/HttpEntity.cs54
-rw-r--r--lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs16
-rw-r--r--lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs88
-rw-r--r--lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs24
-rw-r--r--lib/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj8
13 files changed, 419 insertions, 132 deletions
diff --git a/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs b/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs
index e6b9f24..6c5ebcc 100644
--- a/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs
+++ b/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -29,6 +29,15 @@ using VNLib.Hashing;
using VNLib.Utils;
using VNLib.Utils.Memory;
+/*
+ * Some stuff to note
+ *
+ * Functions have explicit parameters to avoid accidental buffer mixup
+ * when calling nested/overload functions. Please keep it that way for now
+ * I really want to avoid a whoopsie in password hasing.
+ */
+
+
namespace VNLib.Plugins.Essentials.Accounts
{
@@ -59,7 +68,8 @@ namespace VNLib.Plugins.Essentials.Accounts
/// <param name="secret">The password secret provider</param>
/// <param name="setup">The configuration setup arguments</param>
/// <returns>The instance of the library to use</returns>
- public static PasswordHashing Create(IArgon2Library library, ISecretProvider secret, in Argon2ConfigParams setup) => new (library, secret, setup);
+ public static PasswordHashing Create(IArgon2Library library, ISecretProvider secret, in Argon2ConfigParams setup)
+ => new (library, secret, in setup);
/// <summary>
/// Creates a new <see cref="PasswordHashing"/> instance using the default
@@ -69,7 +79,8 @@ namespace VNLib.Plugins.Essentials.Accounts
/// <param name="setup">The configuration setup arguments</param>
/// <returns>The instance of the library to use</returns>
/// <exception cref="DllNotFoundException"></exception>
- public static PasswordHashing Create(ISecretProvider secret, in Argon2ConfigParams setup) => Create(VnArgon2.GetOrLoadSharedLib(), secret, in setup);
+ public static PasswordHashing Create(ISecretProvider secret, in Argon2ConfigParams setup)
+ => Create(VnArgon2.GetOrLoadSharedLib(), secret, in setup);
private Argon2CostParams GetCostParams()
{
@@ -93,15 +104,18 @@ namespace VNLib.Plugins.Essentials.Accounts
if(_secret.BufferSize < STACK_MAX_BUFF_SIZE)
{
- //Alloc stack buffer
+ /*
+ * Also always alloc fixed buffer size again to help
+ * be less obvious during process allocations
+ */
Span<byte> secretBuffer = stackalloc byte[STACK_MAX_BUFF_SIZE];
return VerifyInternal(passHash, password, secretBuffer);
}
else
{
- //Alloc heap buffer
- using UnsafeMemoryHandle<byte> secretBuffer = MemoryUtil.UnsafeAlloc(_secret.BufferSize, true);
+
+ using UnsafeMemoryHandle<byte> secretBuffer = AllocSecretBuffer();
return VerifyInternal(passHash, password, secretBuffer.Span);
}
@@ -111,10 +125,10 @@ namespace VNLib.Plugins.Essentials.Accounts
{
try
{
- //Get the secret from the callback
- ERRNO count = _secret.GetSecret(secretBuffer);
- //Verify
- return _argon2.Verify2id(password, passHash, secretBuffer[..(int)count]);
+
+ ERRNO secretSize = _secret.GetSecret(secretBuffer);
+
+ return _argon2.Verify2id(password, passHash, secretBuffer[..(int)secretSize]);
}
finally
{
@@ -135,11 +149,9 @@ namespace VNLib.Plugins.Essentials.Accounts
public bool Verify(ReadOnlySpan<byte> hash, ReadOnlySpan<byte> salt, ReadOnlySpan<byte> password)
{
if (hash.Length < STACK_MAX_BUFF_SIZE)
- {
- //Alloc stack buffer
+ {
Span<byte> hashBuf = stackalloc byte[hash.Length];
-
- //Hash the password with the current config
+
Hash(password, salt, hashBuf);
//Compare the hashed password to the specified hash and return results
@@ -148,8 +160,7 @@ namespace VNLib.Plugins.Essentials.Accounts
else
{
using UnsafeMemoryHandle<byte> hashBuf = MemoryUtil.UnsafeAlloc(hash.Length, true);
-
- //Hash the password with the current config
+
Hash(password, salt, hashBuf.Span);
//Compare the hashed password to the specified hash and return results
@@ -165,7 +176,7 @@ namespace VNLib.Plugins.Essentials.Accounts
Argon2CostParams costParams = GetCostParams();
//Alloc shared buffer for the salt and secret buffer
- using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAlloc(_config.SaltLen + _secret.BufferSize, true);
+ using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAllocNearestPage(_config.SaltLen + _secret.BufferSize, true);
//Split buffers
Span<byte> saltBuf = buffer.Span[.._config.SaltLen];
@@ -175,12 +186,16 @@ namespace VNLib.Plugins.Essentials.Accounts
RandomHash.GetRandomBytes(saltBuf);
try
- {
- //recover the secret
+ {
ERRNO count = _secret.GetSecret(secretBuf);
-
- //Hashes a password, with the current parameters
- return (PrivateString)_argon2.Hash2id(password, saltBuf, secretBuf[..(int)count], in costParams, _config.HashLen);
+
+ return (PrivateString)_argon2.Hash2id(
+ password: password,
+ salt: saltBuf,
+ secret: secretBuf[..(int)count],
+ costParams: in costParams,
+ hashLen: _config.HashLen
+ );
}
finally
{
@@ -201,7 +216,10 @@ namespace VNLib.Plugins.Essentials.Accounts
Span<byte> saltBuf = buffer.Span[.._config.SaltLen];
Span<byte> secretBuf = buffer.Span[_config.SaltLen..];
- //Fill the buffer with random bytes
+ /*
+ * Salt is just crypographically secure random
+ * data.
+ */
RandomHash.GetRandomBytes(saltBuf);
try
@@ -210,7 +228,13 @@ namespace VNLib.Plugins.Essentials.Accounts
ERRNO count = _secret.GetSecret(secretBuf);
//Hashes a password, with the current parameters
- return (PrivateString)_argon2.Hash2id(password, saltBuf, secretBuf[..(int)count], in costParams, _config.HashLen);
+ return (PrivateString)_argon2.Hash2id(
+ password: password,
+ salt: saltBuf,
+ secret: secretBuf[..(int)count],
+ costParams: in costParams,
+ hashLen: _config.HashLen
+ );
}
finally
{
@@ -229,20 +253,28 @@ namespace VNLib.Plugins.Essentials.Accounts
public void Hash(ReadOnlySpan<byte> password, ReadOnlySpan<byte> salt, Span<byte> hashOutput)
{
Argon2CostParams costParams = GetCostParams();
+
+ using UnsafeMemoryHandle<byte> secretBuffer = AllocSecretBuffer();
- //alloc secret buffer
- using UnsafeMemoryHandle<byte> secretBuffer = MemoryUtil.UnsafeAllocNearestPage(_secret.BufferSize, true);
try
{
- //Get the secret from the callback
- ERRNO count = _secret.GetSecret(secretBuffer.Span);
- //Hashes a password, with the current parameters
- _argon2.Hash2id(password, salt, secretBuffer.Span[..(int)count], hashOutput, in costParams);
+ ERRNO secretSize = _secret.GetSecret(secretBuffer.Span);
+
+ _argon2.Hash2id(
+ password: password,
+ salt: salt,
+ secret: secretBuffer.AsSpan(0, secretSize),
+ rawHashOutput: hashOutput,
+ costParams: in costParams
+ );
}
finally
{
//Erase secret buffer
- MemoryUtil.InitializeBlock(ref secretBuffer.GetReference(), secretBuffer.IntLength);
+ MemoryUtil.InitializeBlock(
+ ref secretBuffer.GetReference(),
+ secretBuffer.IntLength
+ );
}
}
@@ -290,6 +322,12 @@ namespace VNLib.Plugins.Essentials.Accounts
}
}
+ /*
+ * Always alloc page aligned to help keep block allocations
+ * a little less obvious.
+ */
+ private UnsafeMemoryHandle<byte> AllocSecretBuffer() => MemoryUtil.UnsafeAllocNearestPage(_secret.BufferSize, true);
+
private readonly ref struct HashBufferSegments
{
public readonly Span<byte> SaltBuffer;
diff --git a/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs b/lib/Plugins.Essentials/src/Endpoints/SemiConsistentVeTable.cs
index e1706f4..09ab151 100644
--- a/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs
+++ b/lib/Plugins.Essentials/src/Endpoints/SemiConsistentVeTable.cs
@@ -31,11 +31,10 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using VNLib.Net.Http;
-using VNLib.Plugins.Essentials.Endpoints;
-namespace VNLib.Plugins.Essentials
+namespace VNLib.Plugins.Essentials.Endpoints
{
- internal class SemiConsistentVeTable : IVirtualEndpointTable
+ internal sealed class SemiConsistentVeTable : IVirtualEndpointTable
{
/*
@@ -170,7 +169,7 @@ namespace VNLib.Plugins.Essentials
}
///<inheritdoc/>
- public bool TryGetEndpoint(string path, [NotNullWhen(true)] out IVirtualEndpoint<HttpEntity>? endpoint)
+ public bool TryGetEndpoint(string path, [NotNullWhen(true)] out IVirtualEndpoint<HttpEntity>? endpoint)
=> VirtualEndpoints.TryGetValue(path, out endpoint);
diff --git a/lib/Plugins.Essentials/src/EventProcessor.cs b/lib/Plugins.Essentials/src/EventProcessor.cs
index f052c56..ba7aa3c 100644
--- a/lib/Plugins.Essentials/src/EventProcessor.cs
+++ b/lib/Plugins.Essentials/src/EventProcessor.cs
@@ -28,7 +28,6 @@ using System.Net;
using System.Threading;
using System.Net.Sockets;
using System.Threading.Tasks;
-using System.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
@@ -40,8 +39,8 @@ using VNLib.Plugins.Essentials.Accounts;
using VNLib.Plugins.Essentials.Content;
using VNLib.Plugins.Essentials.Sessions;
using VNLib.Plugins.Essentials.Extensions;
-using VNLib.Plugins.Essentials.Middleware;
using VNLib.Plugins.Essentials.Endpoints;
+using VNLib.Plugins.Essentials.Middleware;
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
@@ -133,7 +132,8 @@ namespace VNLib.Plugins.Essentials
/// The internal service pool for the processor
/// </summary>
protected readonly HttpProcessorServicePool ServicePool = new([
- typeof(ISessionProvider), //Order must match the indexes above
+ //Order must match the indexes above
+ typeof(ISessionProvider),
typeof(IPageRouter),
typeof(IAccountSecurityProvider)
]);
@@ -157,6 +157,8 @@ namespace VNLib.Plugins.Essentials
get => ServicePool.ExchangeVersion(ref _accountSec, SEC_INDEX);
}
+ private readonly MiddlewareController _middleware = new(config);
+
///<inheritdoc/>
public virtual async ValueTask ClientConnectedAsync(IHttpEvent httpEvent)
{
@@ -169,8 +171,6 @@ namespace VNLib.Plugins.Essentials
ISessionProvider? sessions = ServicePool.ExchangeVersion(ref _sessions, SESS_INDEX);
IPageRouter? router = ServicePool.ExchangeVersion(ref _router, ROUTER_INDEX);
- LinkedListNode<IHttpMiddleware>? mwNode = config.MiddlewareChain.GetCurrentHead();
-
//event cancellation token
HttpEntity entity = new(httpEvent, this);
@@ -205,24 +205,10 @@ namespace VNLib.Plugins.Essentials
goto RespondAndExit;
}
- //Loop through nodes
- while(mwNode != null)
+ //Exec middleware
+ if(!await _middleware.ProcessAsync(entity))
{
- //Invoke mw handler on our event
- entity.EventArgs = await mwNode.ValueRef.ProcessAsync(entity);
-
- switch (entity.EventArgs.Routine)
- {
- //move next if continue is returned
- case FpRoutine.Continue:
- break;
-
- //Middleware completed the connection, time to exit
- default:
- goto RespondAndExit;
- }
-
- mwNode = mwNode.Next;
+ goto RespondAndExit;
}
if (!config.EndpointTable.IsEmpty)
@@ -257,6 +243,9 @@ namespace VNLib.Plugins.Essentials
RespondAndExit:
+ //Normal post-process
+ _middleware.PostProcess(entity);
+
//Call post processor method
PostProcessEntity(entity, ref entity.EventArgs);
}
@@ -744,5 +733,5 @@ namespace VNLib.Plugins.Essentials
return arr;
}
}
- }
-} \ No newline at end of file
+ }
+}
diff --git a/lib/Plugins.Essentials/src/EventProcessorConfig.cs b/lib/Plugins.Essentials/src/EventProcessorConfig.cs
index 8f401ac..6e101eb 100644
--- a/lib/Plugins.Essentials/src/EventProcessorConfig.cs
+++ b/lib/Plugins.Essentials/src/EventProcessorConfig.cs
@@ -29,6 +29,7 @@ using System.Collections.Frozen;
using System.Collections.Generic;
using VNLib.Utils.Logging;
+using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Essentials.Middleware;
namespace VNLib.Plugins.Essentials
diff --git a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
index a99b1ab..64a9611 100644
--- a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
+++ b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
@@ -114,6 +114,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <returns>true if the connection accepts any content typ, false otherwise</returns>
private static bool AcceptsAny(IConnectionInfo server)
{
+ // If no accept header is sent by clients, it is assumed it accepts all content types
if(server.Accept.Count == 0)
{
return true;
@@ -196,14 +197,15 @@ namespace VNLib.Plugins.Essentials.Extensions
//Alloc enough space to hold the string
Span<char> buffer = stackalloc char[64];
ForwardOnlyWriter<char> rangeBuilder = new(buffer);
+
//Build the range header in this format "bytes <begin>-<end>/<total>"
- rangeBuilder.Append("bytes ");
+ rangeBuilder.AppendSmall("bytes ");
rangeBuilder.Append(start);
rangeBuilder.Append('-');
rangeBuilder.Append(end);
rangeBuilder.Append('/');
rangeBuilder.Append(length);
- //Print to a string and set the content range header
+
entity.Server.Headers[HttpResponseHeader.ContentRange] = rangeBuilder.ToString();
}
@@ -212,7 +214,8 @@ namespace VNLib.Plugins.Essentials.Extensions
/// </summary>
/// <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);
+ public static bool IsCors(this IConnectionInfo server)
+ => string.Equals("cors", server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Determines if the User-Agent specified "cross-site" in the Sec-Site header, OR
@@ -223,8 +226,8 @@ namespace VNLib.Plugins.Essentials.Extensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsCrossSite(this IConnectionInfo server)
{
- return "cross-site".Equals(server.Headers[SEC_HEADER_SITE], StringComparison.OrdinalIgnoreCase)
- || (server.Origin != null && !server.RequestUri.DnsSafeHost.Equals(server.Origin.DnsSafeHost, StringComparison.Ordinal));
+ return string.Equals("cross-site", server.Headers[SEC_HEADER_SITE], StringComparison.OrdinalIgnoreCase)
+ || (server.Origin != null && ! string.Equals(server.RequestUri.DnsSafeHost, server.Origin.DnsSafeHost, StringComparison.Ordinal));
}
/// <summary>
@@ -233,14 +236,16 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="server"></param>
/// <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);
+ public static bool IsUserInvoked(this IConnectionInfo server)
+ => string.Equals("?1", 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);
+ public static bool IsNavigation(this IConnectionInfo server)
+ => string.Equals("navigate", 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
@@ -302,7 +307,11 @@ namespace VNLib.Plugins.Essentials.Extensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool RefererMatch(this IConnectionInfo server)
{
- return server.RequestUri.DnsSafeHost.Equals(server.Referer?.DnsSafeHost, StringComparison.OrdinalIgnoreCase);
+ return string.Equals(
+ server.RequestUri.DnsSafeHost,
+ server.Referer?.DnsSafeHost,
+ StringComparison.OrdinalIgnoreCase
+ );
}
/// <summary>
@@ -315,9 +324,25 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="sameSite"></param>
/// <param name="secure"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void ExpireCookie(this IConnectionInfo server, string name, string domain = "", string path = "/", CookieSameSite sameSite = CookieSameSite.None, bool secure = false)
+ public static void ExpireCookie(
+ this IConnectionInfo server,
+ string name,
+ string domain = "",
+ string path = "/",
+ CookieSameSite sameSite = CookieSameSite.None,
+ bool secure = false
+ )
{
- server.SetCookie(name, string.Empty, domain, path, TimeSpan.Zero, sameSite, false, secure);
+ SetCookie(
+ server: server,
+ name: name,
+ value: string.Empty,
+ domain: domain,
+ path: path,
+ expires: TimeSpan.Zero,
+ sameSite: sameSite,
+ secure: secure
+ );
}
/// <summary>
@@ -340,9 +365,20 @@ namespace VNLib.Plugins.Essentials.Extensions
string path = "/",
CookieSameSite sameSite = CookieSameSite.None,
bool httpOnly = false,
- bool secure = false)
+ bool secure = false
+ )
{
- server.SetCookie(name, value, domain, path, TimeSpan.MaxValue, sameSite, httpOnly, secure);
+ SetCookie(
+ server: server,
+ name: name,
+ value: value,
+ domain: domain,
+ path: path,
+ expires: TimeSpan.Zero,
+ sameSite: sameSite,
+ httpOnly: httpOnly,
+ secure: secure
+ );
}
/// <summary>
@@ -367,9 +403,24 @@ namespace VNLib.Plugins.Essentials.Extensions
string path = "/",
CookieSameSite sameSite = CookieSameSite.None,
bool httpOnly = false,
- bool secure = false)
+ bool secure = false
+ )
{
- server.SetCookie(name, value, domain, path, expires, sameSite, httpOnly, secure);
+
+ HttpResponseCookie cookie = new(name)
+ {
+ Value = value,
+ Domain = domain,
+ Path = path,
+ MaxAge = expires,
+ IsSession = expires == TimeSpan.MaxValue,
+ //If the connection is cross origin, then we need to modify the secure and samsite values
+ SameSite = sameSite,
+ HttpOnly = httpOnly,
+ Secure = secure | server.CrossOrigin,
+ };
+
+ server.SetCookie(in cookie);
}
@@ -380,35 +431,24 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="cookie">The cookie to set for the server</param>
/// <exception cref="ArgumentException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [Obsolete("HttpCookie type is obsolete in favor of HttpResponseCookie")]
public static void SetCookie(this IConnectionInfo server, in HttpCookie cookie)
{
- //Cookie name is required
- if(string.IsNullOrWhiteSpace(cookie.Name))
- {
- throw new ArgumentException("A nonn-null cookie name is required");
- }
-
//Set the cookie
- server.SetCookie(cookie.Name,
- cookie.Value,
- cookie.Domain,
- cookie.Path,
- cookie.ValidFor,
- cookie.SameSite,
- cookie.HttpOnly,
- cookie.Secure);
- }
-
- /// <summary>
- /// Is the current connection a "browser" ?
- /// </summary>
- /// <param name="server"></param>
- /// <returns>true if the user agent string contains "Mozilla" and does not contain "bot", false otherwise</returns>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsBrowser(this IConnectionInfo server)
- {
- //Get user-agent and determine if its a browser
- return server.UserAgent != null && !server.UserAgent.Contains("bot", StringComparison.OrdinalIgnoreCase) && server.UserAgent.Contains("Mozilla", StringComparison.OrdinalIgnoreCase);
+ HttpResponseCookie rCookie = new(cookie.Name)
+ {
+ Value = cookie.Value,
+ Domain = cookie.Domain,
+ Path = cookie.Path,
+ MaxAge = cookie.ValidFor,
+ IsSession = cookie.ValidFor == TimeSpan.MaxValue,
+ //If the connection is cross origin, then we need to modify the secure and samsite values
+ SameSite = cookie.SameSite,
+ HttpOnly = cookie.HttpOnly,
+ Secure = cookie.Secure | server.CrossOrigin,
+ };
+
+ server.SetCookie(in rCookie);
}
/// <summary>
@@ -417,12 +457,9 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="server"></param>
/// <returns>True of the connection was made from the local machine</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsLoopBack(this IConnectionInfo server)
- {
- IPAddress realIp = server.GetTrustedIp();
- return IPAddress.Any.Equals(realIp) || IPAddress.Loopback.Equals(realIp);
- }
-
+ public static bool IsLoopBack(this IConnectionInfo server)
+ => IPAddress.Loopback.Equals(GetTrustedIp(server));
+
/// <summary>
/// Did the connection set the dnt header?
/// </summary>
@@ -493,7 +530,7 @@ namespace VNLib.Plugins.Essentials.Extensions
//Standard https protocol header
string? protocol = server.Headers[X_FORWARDED_PROTO_HEADER];
//If the header is set and equals https then tls is being used
- return string.IsNullOrWhiteSpace(protocol) ? isSecure : "https".Equals(protocol, StringComparison.OrdinalIgnoreCase);
+ return string.IsNullOrWhiteSpace(protocol) ? isSecure : string.Equals("https", protocol, StringComparison.OrdinalIgnoreCase);
}
else
{
diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
index 8adf883..0ca5b8f 100644
--- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
+++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
@@ -796,6 +796,66 @@ namespace VNLib.Plugins.Essentials.Extensions
//Parse the file using the specified parser
return parser(file.FileData, file.ContentTypeString());
}
+
+ /// <summary>
+ /// Reads the contents of an uploaded file at the desired intex into memory
+ /// and returns a managed byte array containing the file data
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="uploadIndex">The index of the uploaded file to buffer</param>
+ /// <returns>A value task that resolves the uploaded data</returns>
+ /// <exception cref="IOException"></exception>
+ public static ValueTask<byte[]> ReadFileDataAsync(this HttpEntity ev, int uploadIndex = 0)
+ {
+ ArgumentNullException.ThrowIfNull(ev);
+
+ /*
+ * File should exist at the desired index and have a length greater than 0
+ * otherwise return an empty buffer
+ */
+ if (ev.Files.Count <= uploadIndex || ev.Files[uploadIndex].Length == 0)
+ {
+ return ValueTask.FromResult(Array.Empty<byte>());
+ }
+
+ return ReadFileDataAsync(ev, uploadIndex);
+
+ static async ValueTask<byte[]> ReadFileDataAsync(HttpEntity entity, int fileIndex)
+ {
+ FileUpload upload = entity.Files[fileIndex];
+
+ /*
+ * Alloc an uninitialized buffer to read the file data into, it should ALL
+ * be overwritten during read operation
+ */
+ byte[] buffer = GC.AllocateUninitializedArray<byte>((int)upload.Length);
+
+ int read = 0;
+
+ do
+ {
+ Memory<byte> mem = buffer.AsMemory(read, buffer.Length - read);
+
+ int r = await upload.FileData.ReadAsync(mem, entity.EventCancellation);
+
+ //If no data was read force break and deal with read data
+ if (r == 0)
+ {
+ break;
+ }
+
+ read += r;
+ } while (read < buffer.Length);
+
+ //Buffer is exact length, so read should be equal to length
+ if (read != buffer.Length)
+ {
+ throw new IOException("Failed to read entire file data, this may be an internal error");
+ }
+
+ return buffer;
+ }
+ }
/// <summary>
/// Get a <see cref="DirectoryInfo"/> instance that points to the current sites filesystem root.
@@ -816,12 +876,12 @@ namespace VNLib.Plugins.Essentials.Extensions
public static string ContentTypeString(this in FileUpload upload) => HttpHelpers.GetContentTypeString(upload.ContentType);
/// <summary>
- /// Sets the <see cref="HttpControlMask.CompressionDisabed"/> flag on the current
+ /// Sets the <see cref="HttpControlMask.CompressionDisabled"/> flag on the current
/// <see cref="IHttpEvent"/> instance to disable dynamic compression on the response.
/// </summary>
/// <param name="entity"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void DisableCompression(this IHttpEvent entity) => entity.SetControlFlag(HttpControlMask.CompressionDisabed);
+ public static void DisableCompression(this IHttpEvent entity) => entity.SetControlFlag(HttpControlMask.CompressionDisabled);
/// <summary>
/// Attempts to upgrade the connection to a websocket, if the setup fails, it sets up the response to the client accordingly.
diff --git a/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs b/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs
index 6158a69..19c8e78 100644
--- a/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs
+++ b/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -33,6 +33,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// </summary>
/// <param name="Name">The cookie name</param>
/// <param name="Value">The cookie value</param>
+ [Obsolete("Obsolete in favor of HttpResponseCookie")]
public readonly record struct HttpCookie (string Name, string Value)
{
/// <summary>
diff --git a/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs b/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs
index f3b02dc..ef86934 100644
--- a/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs
+++ b/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs
@@ -94,15 +94,19 @@ namespace VNLib.Plugins.Essentials.Extensions
//Only set cooke if already exists or force is true
if (entity.Server.RequestCookies.ContainsKey(Name) || force)
{
- //Build and set cookie
- HttpCookie cookie = new(Name, value)
+ HttpResponseCookie cookie = new(Name)
{
- Secure = Secure,
- HttpOnly = HttpOnly,
- ValidFor = ValidFor,
- SameSite = SameSite,
+ Value = value,
+ Domain = Domain,
Path = Path,
- Domain = Domain
+ //Only set max-age if cookie has a value, otherwise set to zero to expire
+ MaxAge = string.IsNullOrWhiteSpace(value) ? TimeSpan.Zero : ValidFor,
+ IsSession = ValidFor == TimeSpan.MaxValue,
+ SameSite = SameSite,
+ HttpOnly = HttpOnly,
+
+ //Secure is required on cross origin requests
+ Secure = Secure | entity.Server.CrossOrigin,
};
entity.Server.SetCookie(in cookie);
diff --git a/lib/Plugins.Essentials/src/HttpEntity.cs b/lib/Plugins.Essentials/src/HttpEntity.cs
index 2a24982..ff728e3 100644
--- a/lib/Plugins.Essentials/src/HttpEntity.cs
+++ b/lib/Plugins.Essentials/src/HttpEntity.cs
@@ -26,6 +26,7 @@ using System;
using System.IO;
using System.Net;
using System.Threading;
+using System.Diagnostics;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@@ -204,6 +205,27 @@ namespace VNLib.Plugins.Essentials
throw new ContentTypeUnacceptableException("The client does not accept the content type of the response");
}
+ /*
+ * If the underlying stream is actaully a memory stream,
+ * create a wrapper for it to read as a memory response.
+ * This is done to avoid a user-space copy since we can
+ * get access to access the internal buffer
+ *
+ * Stream length also should not cause an integer overflow,
+ * which also mean position is assumed not to overflow
+ * or cause an overflow during reading
+ */
+ if(stream is MemoryStream ms && length < int.MaxValue)
+ {
+ Entity.CloseResponse(
+ code,
+ type,
+ new MemStreamWrapper(ms, (int)length)
+ );
+
+ return;
+ }
+
Entity.CloseResponse(code, type, stream, length);
}
@@ -246,5 +268,37 @@ namespace VNLib.Plugins.Essentials
///<inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void IHttpEvent.DangerousChangeProtocol(IAlternateProtocol protocolHandler) => Entity.DangerousChangeProtocol(protocolHandler);
+
+
+ private sealed class MemStreamWrapper(MemoryStream memStream, int length) : IMemoryResponseReader
+ {
+ readonly int length = length;
+
+ /*
+ * Stream may be offset by the caller, it needs
+ * to be respected during streaming.
+ */
+ int read = (int)memStream.Position;
+
+ public int Remaining
+ {
+ get
+ {
+ Debug.Assert(length - read >= 0);
+ return length - read;
+ }
+ }
+
+ public void Advance(int written) => read += written;
+
+ ///<inheritdoc/>
+ public void Close() => memStream.Dispose();
+
+ public ReadOnlyMemory<byte> GetMemory()
+ {
+ byte[] intBuffer = memStream.GetBuffer();
+ return new ReadOnlyMemory<byte>(intBuffer, read, Remaining);
+ }
+ }
}
}
diff --git a/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs b/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs
index 83e6a06..990d59b 100644
--- a/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs
+++ b/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -40,5 +40,19 @@ namespace VNLib.Plugins.Essentials.Middleware
/// <param name="entity">The entity to process</param>
/// <returns>The result of the operation</returns>
ValueTask<FileProcessArgs> ProcessAsync(HttpEntity entity);
+
+ /// <summary>
+ /// Post processes an HTTP entity with possible file selection. May optionally mutate the
+ /// current arguments before the event processor completes a response.
+ /// </summary>
+ /// <param name="entity">The entity that has been processes and is ready to close</param>
+ /// <param name="currentArgs">The current file processor arguments</param>
+ /// <remarks>
+ /// Generally this function should simply observe results as the entity may already have been
+ /// configured for a response, such as by a virtual routine. You should inspect the current arguments
+ /// before mutating the reference.
+ /// </remarks>
+ virtual void VolatilePostProcess(HttpEntity entity, ref FileProcessArgs currentArgs)
+ { }
}
}
diff --git a/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs b/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs
new file mode 100644
index 0000000..c3a85c9
--- /dev/null
+++ b/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs
@@ -0,0 +1,88 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: MiddlewareController.cs
+*
+* MiddlewareController.cs is part of VNLib.Plugins.Essentials which is part
+* of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+namespace VNLib.Plugins.Essentials.Middleware
+{
+ internal sealed class MiddlewareController(EventProcessorConfig config)
+ {
+ private readonly IHttpMiddlewareChain _chain = config.MiddlewareChain;
+
+ public async ValueTask<bool> ProcessAsync(HttpEntity entity)
+ {
+ /*
+ * Loops through the current linkedlist of the current middleware chain. The
+ * chain should remain unmodified after GetCurrentHead() is called.
+ *
+ * Middleware will return a Continue routine to move to the next middleware
+ * node. All other routines mean the processor has responded to the client
+ * itself and must exit control and move to response.
+ */
+
+ LinkedListNode<IHttpMiddleware>? mwNode = _chain.GetCurrentHead();
+
+ //Loop through nodes
+ while (mwNode != null)
+ {
+ entity.EventArgs = await mwNode.ValueRef.ProcessAsync(entity);
+
+ switch (entity.EventArgs.Routine)
+ {
+ //move next if continue is returned
+ case FpRoutine.Continue:
+ break;
+
+ //Middleware completed the connection, time to exit the event processing
+ default:
+ return false;
+ }
+
+ mwNode = mwNode.Next;
+ }
+
+ return true;
+ }
+
+ public void PostProcess(HttpEntity entity)
+ {
+ /*
+ * Middleware nodes may be allowed to inspect, or modify the return
+ * event arguments as the server may not have responded to the client
+ * yet.
+ */
+
+ LinkedListNode<IHttpMiddleware>? mwNode = _chain.GetCurrentHead();
+
+ while (mwNode != null)
+ {
+ //Invoke mw handler on our event
+ mwNode.ValueRef.VolatilePostProcess(entity, ref entity.EventArgs);
+
+ mwNode = mwNode.Next;
+ }
+ }
+ }
+}
diff --git a/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs b/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs
index e65c26d..b60c7c3 100644
--- a/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs
+++ b/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs
@@ -90,40 +90,40 @@ namespace VNLib.Plugins.Essentials.Oauth
ForwardOnlyWriter<char> writer = new(buffer.Span);
//Build the error message string
- writer.Append("{\"error\":\"");
+ writer.AppendSmall("{\"error\":\"");
switch (error)
{
case ErrorType.InvalidRequest:
- writer.Append("invalid_request");
+ writer.AppendSmall("invalid_request");
break;
case ErrorType.InvalidClient:
- writer.Append("invalid_client");
+ writer.AppendSmall("invalid_client");
break;
case ErrorType.UnauthorizedClient:
- writer.Append("unauthorized_client");
+ writer.AppendSmall("unauthorized_client");
break;
case ErrorType.InvalidToken:
- writer.Append("invalid_token");
+ writer.AppendSmall("invalid_token");
break;
case ErrorType.UnsupportedResponseType:
- writer.Append("unsupported_response_type");
+ writer.AppendSmall("unsupported_response_type");
break;
case ErrorType.InvalidScope:
- writer.Append("invalid_scope");
+ writer.AppendSmall("invalid_scope");
break;
case ErrorType.ServerError:
- writer.Append("server_error");
+ writer.AppendSmall("server_error");
break;
case ErrorType.TemporarilyUnavailable:
- writer.Append("temporarily_unavailable");
+ writer.AppendSmall("temporarily_unavailable");
break;
default:
- writer.Append("error");
+ writer.AppendSmall("error");
break;
}
- writer.Append("\",\"error_description\":\"");
+ writer.AppendSmall("\",\"error_description\":\"");
writer.Append(description);
- writer.Append("\"}");
+ writer.AppendSmall("\"}");
//Close the response with the json data
ev.CloseResponse(code, ContentType.Json, writer.AsSpan());
diff --git a/lib/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj b/lib/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj
index 4da640a..3a21ac9 100644
--- a/lib/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj
+++ b/lib/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj
@@ -2,13 +2,15 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
+ <Nullable>enable</Nullable>
<RootNamespace>VNLib.Plugins.Essentials</RootNamespace>
<AssemblyName>VNLib.Plugins.Essentials</AssemblyName>
- <AnalysisLevel>latest-all</AnalysisLevel>
- <EnableNETAnalyzers>True</EnableNETAnalyzers>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
- <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <AnalysisLevel Condition="'$(BuildingInsideVisualStudio)' == true">latest-all</AnalysisLevel>
</PropertyGroup>
<PropertyGroup>