diff options
Diffstat (limited to 'lib/Plugins.Essentials')
-rw-r--r-- | lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs | 100 | ||||
-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.cs | 37 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/EventProcessorConfig.cs | 1 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs | 131 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs | 64 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Extensions/HttpCookie.cs | 3 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs | 18 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/HttpEntity.cs | 54 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs | 16 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs | 88 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs | 24 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/VNLib.Plugins.Essentials.csproj | 8 |
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> |