aboutsummaryrefslogtreecommitdiff
path: root/Plugins.Essentials/src/Extensions
diff options
context:
space:
mode:
Diffstat (limited to 'Plugins.Essentials/src/Extensions')
-rw-r--r--Plugins.Essentials/src/Extensions/CollectionsExtensions.cs85
-rw-r--r--Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs361
-rw-r--r--Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs848
-rw-r--r--Plugins.Essentials/src/Extensions/IJsonSerializerBuffer.cs48
-rw-r--r--Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs100
-rw-r--r--Plugins.Essentials/src/Extensions/InvalidJsonRequestException.cs57
-rw-r--r--Plugins.Essentials/src/Extensions/JsonResponse.cs112
-rw-r--r--Plugins.Essentials/src/Extensions/RedirectType.cs37
-rw-r--r--Plugins.Essentials/src/Extensions/SimpleMemoryResponse.cs89
-rw-r--r--Plugins.Essentials/src/Extensions/UserExtensions.cs94
10 files changed, 1831 insertions, 0 deletions
diff --git a/Plugins.Essentials/src/Extensions/CollectionsExtensions.cs b/Plugins.Essentials/src/Extensions/CollectionsExtensions.cs
new file mode 100644
index 0000000..9500d5e
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/CollectionsExtensions.cs
@@ -0,0 +1,85 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: CollectionsExtensions.cs
+*
+* CollectionsExtensions.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.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ /// <summary>
+ ///
+ /// </summary>
+ public static class CollectionsExtensions
+ {
+ /// <summary>
+ /// Gets a value by the specified key if it exsits and the value is not null/empty
+ /// </summary>
+ /// <param name="dict"></param>
+ /// <param name="key">Key associated with the value</param>
+ /// <param name="value">Value associated with the key</param>
+ /// <returns>True of the key is found and is not noll/empty, false otherwise</returns>
+ public static bool TryGetNonEmptyValue(this IReadOnlyDictionary<string, string> dict, string key, [MaybeNullWhen(false)] out string value)
+ {
+ if (dict.TryGetValue(key, out string? val) && !string.IsNullOrWhiteSpace(val))
+ {
+ value = val;
+ return true;
+ }
+ value = null;
+ return false;
+ }
+ /// <summary>
+ /// Determines if an argument was set in a <see cref="IReadOnlyDictionary{TKey, TValue}"/> by comparing
+ /// the value stored at the key, to the type argument
+ /// </summary>
+ /// <param name="dict"></param>
+ /// <param name="key">The argument's key</param>
+ /// <param name="argument">The argument to compare against</param>
+ /// <returns>
+ /// True if the key was found, and the value at the key is equal to the type parameter. False if the key is null/empty, or the
+ /// value does not match the specified type
+ /// </returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static bool IsArgumentSet(this IReadOnlyDictionary<string, string> dict, string key, ReadOnlySpan<char> argument)
+ {
+ //Try to get the value from the dict, if the value is null casting it to span (implicitly) should stop null excpetions and return false
+ return dict.TryGetValue(key, out string? value) && string.GetHashCode(argument) == string.GetHashCode(value);
+ }
+ /// <summary>
+ ///
+ /// </summary>
+ /// <typeparam name="TKey"></typeparam>
+ /// <typeparam name="TValue"></typeparam>
+ /// <param name="dict"></param>
+ /// <param name="key"></param>
+ /// <returns></returns>
+ public static TValue? GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key)
+ {
+ return dict.TryGetValue(key, out TValue? value) ? value : default;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs b/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
new file mode 100644
index 0000000..ba01132
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
@@ -0,0 +1,361 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: ConnectionInfoExtensions.cs
+*
+* ConnectionInfoExtensions.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.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Runtime.CompilerServices;
+
+using VNLib.Net.Http;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ /// <summary>
+ /// Provides <see cref="ConnectionInfo"/> extension methods
+ /// for common use cases
+ /// </summary>
+ public static class IConnectionInfoExtensions
+ {
+ public const string SEC_HEADER_MODE = "Sec-Fetch-Mode";
+ public const string SEC_HEADER_SITE = "Sec-Fetch-Site";
+ public const string SEC_HEADER_USER = "Sec-Fetch-User";
+ public const string SEC_HEADER_DEST = "Sec-Fetch-Dest";
+ public const string X_FORWARDED_FOR_HEADER = "x-forwarded-for";
+ public const string X_FORWARDED_PROTO_HEADER = "x-forwarded-proto";
+ public const string DNT_HEADER = "dnt";
+
+ /// <summary>
+ /// Cache-Control header value for disabling cache
+ /// </summary>
+ public static readonly string NO_CACHE_RESPONSE_HEADER_VALUE = HttpHelpers.GetCacheString(CacheType.NoCache | CacheType.NoStore | CacheType.Revalidate);
+
+ /// <summary>
+ /// Gets the <see cref="HttpRequestHeader.IfModifiedSince"/> header value and converts its value to a datetime value
+ /// </summary>
+ /// <returns>The if modified-since header date-time, null if the header was not set or the value was invalid</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static DateTimeOffset? LastModified(this IConnectionInfo server)
+ {
+ //Get the if-modified-since header
+ string? ifModifiedSince = server.Headers[HttpRequestHeader.IfModifiedSince];
+ //Make sure tis set and try to convert it to a date-time structure
+ return DateTimeOffset.TryParse(ifModifiedSince, out DateTimeOffset d) ? d : null;
+ }
+
+ /// <summary>
+ /// Sets the last-modified response header value
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="value">Time the entity was last modified</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void LastModified(this IConnectionInfo server, DateTimeOffset value)
+ {
+ server.Headers[HttpResponseHeader.LastModified] = value.ToString("R");
+ }
+
+ /// <summary>
+ /// Is the connection requesting cors
+ /// </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);
+ /// <summary>
+ /// Determines if the User-Agent specified "cross-site" in the Sec-Site header, OR
+ /// the connection spcified an origin header and the origin's host does not match the
+ /// requested host
+ /// </summary>
+ /// <returns>true if the request originated from a site other than the current one</returns>
+ [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));
+ }
+ /// <summary>
+ /// Is the connection user-agent created, or automatic
+ /// </summary>
+ /// <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);
+ /// <summary>
+ /// Was this request created from normal user navigation
+ /// </summary>
+ /// <returns>true if sec-mode set to "navigate"</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsNavigation(this IConnectionInfo server) => "navigate".Equals(server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
+ /// <summary>
+ /// Determines if the client specified "no-cache" for the cache control header, signalling they do not wish to cache the entity
+ /// </summary>
+ /// <returns>True if <see cref="HttpRequestHeader.CacheControl"/> contains the string "no-cache", false otherwise</returns>
+ public static bool NoCache(this IConnectionInfo server)
+ {
+ string? cache_header = server.Headers[HttpRequestHeader.CacheControl];
+ return !string.IsNullOrWhiteSpace(cache_header) && cache_header.Contains("no-cache", StringComparison.OrdinalIgnoreCase);
+ }
+ /// <summary>
+ /// Sets the response cache headers to match the requested caching type. Does not check against request headers
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="type">One or more <see cref="CacheType"/> flags that identify the way the entity can be cached</param>
+ /// <param name="maxAge">The max age the entity is valid for</param>
+ public static void SetCache(this IConnectionInfo server, CacheType type, TimeSpan maxAge)
+ {
+ //If no cache flag is set, set the pragma header to no-cache
+ if((type & CacheType.NoCache) > 0)
+ {
+ server.Headers[HttpResponseHeader.Pragma] = "no-cache";
+ }
+ //Set the cache hader string using the http helper class
+ server.Headers[HttpResponseHeader.CacheControl] = HttpHelpers.GetCacheString(type, maxAge);
+ }
+ /// <summary>
+ /// Sets the Cache-Control response header to <see cref="NO_CACHE_RESPONSE_HEADER_VALUE"/>
+ /// and the pragma response header to 'no-cache'
+ /// </summary>
+ /// <param name="server"></param>
+ public static void SetNoCache(this IConnectionInfo server)
+ {
+ //Set default nocache string
+ server.Headers[HttpResponseHeader.CacheControl] = NO_CACHE_RESPONSE_HEADER_VALUE;
+ server.Headers[HttpResponseHeader.Pragma] = "no-cache";
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the port number in the request is equivalent to the port number
+ /// on the local server.
+ /// </summary>
+ /// <returns>True if the port number in the <see cref="ConnectionInfo.RequestUri"/> matches the
+ /// <see cref="ConnectionInfo.LocalEndpoint"/> port false if they do not match
+ /// </returns>
+ /// <remarks>
+ /// Users should call this method to help prevent port based attacks if your
+ /// code relies on the port number of the <see cref="ConnectionInfo.RequestUri"/>
+ /// </remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool EnpointPortsMatch(this IConnectionInfo server)
+ {
+ return server.RequestUri.Port == server.LocalEndpoint.Port;
+ }
+ /// <summary>
+ /// Determines if the host of the current request URI matches the referer header host
+ /// </summary>
+ /// <returns>True if the request host and the referer host paremeters match, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool RefererMatch(this IConnectionInfo server)
+ {
+ return server.RequestUri.DnsSafeHost.Equals(server.Referer?.DnsSafeHost, StringComparison.OrdinalIgnoreCase);
+ }
+ /// <summary>
+ /// Expires a client's cookie
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="name"></param>
+ /// <param name="domain"></param>
+ /// <param name="path"></param>
+ /// <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)
+ {
+ server.SetCookie(name, string.Empty, domain, path, TimeSpan.Zero, sameSite, false, secure);
+ }
+ /// <summary>
+ /// Sets a cookie with an infinite (session life-span)
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="name"></param>
+ /// <param name="value"></param>
+ /// <param name="domain"></param>
+ /// <param name="path"></param>
+ /// <param name="sameSite"></param>
+ /// <param name="httpOnly"></param>
+ /// <param name="secure"></param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void SetSessionCookie(
+ this IConnectionInfo server,
+ string name,
+ string value,
+ string domain = "",
+ string path = "/",
+ CookieSameSite sameSite = CookieSameSite.None,
+ bool httpOnly = false,
+ bool secure = false)
+ {
+ server.SetCookie(name, value, domain, path, TimeSpan.MaxValue, sameSite, httpOnly, secure);
+ }
+
+ /// <summary>
+ /// Sets a cookie with an infinite (session life-span)
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="name"></param>
+ /// <param name="value"></param>
+ /// <param name="domain"></param>
+ /// <param name="path"></param>
+ /// <param name="sameSite"></param>
+ /// <param name="httpOnly"></param>
+ /// <param name="secure"></param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void SetCookie(
+ this IConnectionInfo server,
+ string name,
+ string value,
+ TimeSpan expires,
+ string domain = "",
+ string path = "/",
+ CookieSameSite sameSite = CookieSameSite.None,
+ bool httpOnly = false,
+ bool secure = false)
+ {
+ server.SetCookie(name, value, domain, path, expires, sameSite, httpOnly, 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);
+ }
+ /// <summary>
+ /// Determines if the current connection is the loopback/internal network adapter
+ /// </summary>
+ /// <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);
+ }
+
+ /// <summary>
+ /// Did the connection set the dnt header?
+ /// </summary>
+ /// <returns>true if the connection specified the dnt header, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool DNT(this IConnectionInfo server) => !string.IsNullOrWhiteSpace(server.Headers[DNT_HEADER]);
+
+ /// <summary>
+ /// Determins if the current connection is behind a trusted downstream server
+ /// </summary>
+ /// <param name="server"></param>
+ /// <returns>True if the connection came from a trusted downstream server, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsBehindDownStreamServer(this IConnectionInfo server)
+ {
+ //See if there is an ambient event processor
+ EventProcessor? ev = EventProcessor.Current;
+ //See if the connection is coming from an downstream server
+ return ev != null && ev.Options.DownStreamServers.Contains(server.RemoteEndpoint.Address);
+ }
+
+ /// <summary>
+ /// Gets the real IP address of the request if behind a trusted downstream server, otherwise returns the transport remote ip address
+ /// </summary>
+ /// <param name="server"></param>
+ /// <returns>The real ip of the connection</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IPAddress GetTrustedIp(this IConnectionInfo server) => GetTrustedIp(server, server.IsBehindDownStreamServer());
+ /// <summary>
+ /// Gets the real IP address of the request if behind a trusted downstream server, otherwise returns the transport remote ip address
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="isTrusted"></param>
+ /// <returns>The real ip of the connection</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static IPAddress GetTrustedIp(this IConnectionInfo server, bool isTrusted)
+ {
+ //If the connection is not trusted, then ignore header parsing
+ if (isTrusted)
+ {
+ //Nginx sets a header identifying the remote ip address so parse it
+ string? real_ip = server.Headers[X_FORWARDED_FOR_HEADER];
+ //If the real-ip header is set, try to parse is and return the address found, otherwise return the remote ep
+ return !string.IsNullOrWhiteSpace(real_ip) && IPAddress.TryParse(real_ip, out IPAddress? addr) ? addr : server.RemoteEndpoint.Address;
+ }
+ else
+ {
+ return server.RemoteEndpoint.Address;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value that determines if the connection is using tls, locally
+ /// or behind a trusted downstream server that is using tls.
+ /// </summary>
+ /// <param name="server"></param>
+ /// <returns>True if the connection is secure, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsSecure(this IConnectionInfo server)
+ {
+ //Get value of the trusted downstream server
+ return IsSecure(server, server.IsBehindDownStreamServer());
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool IsSecure(this IConnectionInfo server, bool isTrusted)
+ {
+ //If the connection is not trusted, then ignore header parsing
+ if (isTrusted)
+ {
+ //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) ? server.IsSecure : "https".Equals(protocol, StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ return server.IsSecure;
+ }
+ }
+
+ /// <summary>
+ /// Was the connection made on a local network to the server? NOTE: Use with caution
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsLocalConnection(this IConnectionInfo server) => server.LocalEndpoint.Address.IsLocalSubnet(server.GetTrustedIp());
+
+ /// <summary>
+ /// Get a cookie from the current request
+ /// </summary>
+ /// <param name="server"></param>
+ /// <param name="name">Name/ID of cookie</param>
+ /// <param name="cookieValue">Is set to cookie if found, or null if not</param>
+ /// <returns>True if cookie exists and was retrieved</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool GetCookie(this IConnectionInfo server, string name, [NotNullWhen(true)] out string? cookieValue)
+ {
+ //Try to get a cookie from the request
+ return server.RequestCookies.TryGetValue(name, out cookieValue);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
new file mode 100644
index 0000000..9458487
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
@@ -0,0 +1,848 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: EssentialHttpEventExtensions.cs
+*
+* EssentialHttpEventExtensions.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.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+using VNLib.Net.Http;
+using VNLib.Hashing;
+using VNLib.Utils;
+using VNLib.Utils.Extensions;
+using VNLib.Utils.Memory.Caching;
+using static VNLib.Plugins.Essentials.Statics;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+
+ /// <summary>
+ /// Provides extension methods for manipulating <see cref="HttpEvent"/>s
+ /// </summary>
+ public static class EssentialHttpEventExtensions
+ {
+ public const string BEARER_STRING = "Bearer";
+ private static readonly int BEARER_LEN = BEARER_STRING.Length;
+
+ /*
+ * Pooled/tlocal serializers
+ */
+ private static ThreadLocal<Utf8JsonWriter> LocalSerializer { get; } = new(() => new(Stream.Null));
+ private static IObjectRental<JsonResponse> ResponsePool { get; } = ObjectRental.Create(ResponseCtor);
+ private static JsonResponse ResponseCtor() => new(ResponsePool);
+
+ #region Response Configuring
+
+ /// <summary>
+ /// Attempts to serialize the JSON object (with default SR_OPTIONS) to binary and configure the response for a JSON message body
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="ev"></param>
+ /// <param name="code">The <see cref="HttpStatusCode"/> result of the connection</param>
+ /// <param name="response">The JSON object to serialzie and send as response body</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseJson<T>(this IHttpEvent ev, HttpStatusCode code, T response) => CloseResponseJson(ev, code, response, SR_OPTIONS);
+
+ /// <summary>
+ /// Attempts to serialize the JSON object to binary and configure the response for a JSON message body
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="ev"></param>
+ /// <param name="code">The <see cref="HttpStatusCode"/> result of the connection</param>
+ /// <param name="response">The JSON object to serialzie and send as response body</param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during serialization</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseJson<T>(this IHttpEvent ev, HttpStatusCode code, T response, JsonSerializerOptions? options)
+ {
+ JsonResponse rbuf = ResponsePool.Rent();
+ try
+ {
+ //Serialze the object on the thread local serializer
+ LocalSerializer.Value!.Serialize(rbuf, response, options);
+
+ //Set the response as the buffer,
+ ev.CloseResponse(code, ContentType.Json, rbuf);
+ }
+ catch
+ {
+ //Return back to pool on error
+ ResponsePool.Return(rbuf);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Attempts to serialize the JSON object to binary and configure the response for a JSON message body
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The <see cref="HttpStatusCode"/> result of the connection</param>
+ /// <param name="response">The JSON object to serialzie and send as response body</param>
+ /// <param name="type">The type to use during de-serialization</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseJson(this IHttpEvent ev, HttpStatusCode code, object response, Type type) => CloseResponseJson(ev, code, response, type, SR_OPTIONS);
+
+ /// <summary>
+ /// Attempts to serialize the JSON object to binary and configure the response for a JSON message body
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The <see cref="HttpStatusCode"/> result of the connection</param>
+ /// <param name="response">The JSON object to serialzie and send as response body</param>
+ /// <param name="type">The type to use during de-serialization</param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during serialization</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseJson(this IHttpEvent ev, HttpStatusCode code, object response, Type type, JsonSerializerOptions? options)
+ {
+ JsonResponse rbuf = ResponsePool.Rent();
+ try
+ {
+ //Serialze the object on the thread local serializer
+ LocalSerializer.Value!.Serialize(rbuf, response, type, options);
+
+ //Set the response as the buffer,
+ ev.CloseResponse(code, ContentType.Json, rbuf);
+ }
+ catch
+ {
+ //Return back to pool on error
+ ResponsePool.Return(rbuf);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Writes the <see cref="JsonDocument"/> data to a temporary buffer and sets it as the response
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The <see cref="HttpStatusCode"/> result of the connection</param>
+ /// <param name="data">The <see cref="JsonDocument"/> data to send to client</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseJson(this IHttpEvent ev, HttpStatusCode code, JsonDocument data)
+ {
+ if(data == null)
+ {
+ ev.CloseResponse(code);
+ return;
+ }
+
+ JsonResponse rbuf = ResponsePool.Rent();
+ try
+ {
+ //Serialze the object on the thread local serializer
+ LocalSerializer.Value!.Serialize(rbuf, data);
+
+ //Set the response as the buffer,
+ ev.CloseResponse(code, ContentType.Json, rbuf);
+ }
+ catch
+ {
+ //Return back to pool on error
+ ResponsePool.Return(rbuf);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Close as response to a client with an <see cref="HttpStatusCode.OK"/> and serializes a <see cref="WebMessage"/> as the message response
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="webm">The <see cref="WebMessage"/> to serialize and response to client with</param>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponse<T>(this IHttpEvent ev, T webm) where T:WebMessage
+ {
+ if (webm == null)
+ {
+ ev.CloseResponse(HttpStatusCode.OK);
+ }
+ else
+ {
+ //Respond with json data
+ ev.CloseResponseJson(HttpStatusCode.OK, webm);
+ }
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a file as an attachment (set content dispostion)
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">Status code</param>
+ /// <param name="file">The <see cref="FileInfo"/> of the desired file to attach</param>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="FileNotFoundException"></exception>
+ /// <exception cref="UnauthorizedAccessException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ /// <exception cref="System.Security.SecurityException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, FileInfo file)
+ {
+ //Close with file
+ ev.CloseResponse(code, file);
+ //Set content dispostion as attachment (only if successfull)
+ ev.Server.Headers["Content-Disposition"] = $"attachment; filename=\"{file.Name}\"";
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a file as an attachment (set content dispostion)
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">Status code</param>
+ /// <param name="file">The <see cref="FileStream"/> of the desired file to attach</param>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, FileStream file)
+ {
+ //Close with file
+ ev.CloseResponse(code, file);
+ //Set content dispostion as attachment (only if successfull)
+ ev.Server.Headers["Content-Disposition"] = $"attachment; filename=\"{file.Name}\"";
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a file as an attachment (set content dispostion)
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">Status code</param>
+ /// <param name="data">The data to straem to the client as an attatcment</param>
+ /// <param name="ct">The <see cref="ContentType"/> that represents the file</param>
+ /// <param name="fileName">The name of the file to attach</param>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, ContentType ct, Stream data, string fileName)
+ {
+ //Close with file
+ ev.CloseResponse(code, ct, data);
+ //Set content dispostion as attachment (only if successfull)
+ ev.Server.Headers["Content-Disposition"] = $"attachment; filename=\"{fileName}\"";
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a file as the entire response body (not attachment)
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">Status code</param>
+ /// <param name="file">The <see cref="FileInfo"/> of the desired file to attach</param>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="FileNotFoundException"></exception>
+ /// <exception cref="UnauthorizedAccessException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ /// <exception cref="System.Security.SecurityException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, FileInfo file)
+ {
+ //Open filestream for file
+ FileStream fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
+ try
+ {
+ //Set the input as a stream
+ ev.CloseResponse(code, fs);
+ //Set last modified time only if successfull
+ ev.Server.Headers[HttpResponseHeader.LastModified] = file.LastWriteTimeUtc.ToString("R");
+ }
+ catch
+ {
+ //If their is an exception close the stream and re-throw
+ fs.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a <see cref="FileStream"/> as the entire response body (not attachment)
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">Status code</param>
+ /// <param name="file">The <see cref="FileStream"/> of the desired file to attach</param>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, FileStream file)
+ {
+ //Get content type from filename
+ ContentType ct = HttpHelpers.GetContentTypeFromFile(file.Name);
+ //Set the input as a stream
+ ev.CloseResponse(code, ct, file);
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a character buffer using the server wide
+ /// <see cref="ConnectionInfo.Encoding"/> encoding
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The response status code</param>
+ /// <param name="type">The <see cref="ContentType"/> the data represents</param>
+ /// <param name="data">The character buffer to send</param>
+ /// <remarks>This method will store an encoded copy as a memory stream, so be careful with large buffers</remarks>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, in ReadOnlySpan<char> data)
+ {
+ //Get a memory stream using UTF8 encoding
+ CloseResponse(ev, code, type, in data, ev.Server.Encoding);
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a character buffer using the specified encoding type
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The response status code</param>
+ /// <param name="type">The <see cref="ContentType"/> the data represents</param>
+ /// <param name="data">The character buffer to send</param>
+ /// <param name="encoding">The encoding type to use when converting the buffer</param>
+ /// <remarks>This method will store an encoded copy as a memory stream, so be careful with large buffers</remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, in ReadOnlySpan<char> data, Encoding encoding)
+ {
+ if (data.IsEmpty)
+ {
+ ev.CloseResponse(code);
+ return;
+ }
+
+ //Validate encoding
+ _ = encoding ?? throw new ArgumentNullException(nameof(encoding));
+
+ //Get new simple memory response
+ IMemoryResponseReader reader = new SimpleMemoryResponse(data, encoding);
+ ev.CloseResponse(code, type, reader);
+ }
+
+ /// <summary>
+ /// Close a response to a connection by copying the speciifed binary buffer
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The response status code</param>
+ /// <param name="type">The <see cref="ContentType"/> the data represents</param>
+ /// <param name="data">The binary buffer to send</param>
+ /// <remarks>The data paramter is copied into an internal <see cref="IMemoryResponseReader"/></remarks>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan<byte> data)
+ {
+ if (data.IsEmpty)
+ {
+ ev.CloseResponse(code);
+ return;
+ }
+
+ //Get new simple memory response
+ IMemoryResponseReader reader = new SimpleMemoryResponse(data);
+ ev.CloseResponse(code, type, reader);
+ }
+
+ /// <summary>
+ /// Close a response to a connection with a relative file within the current root's directory
+ /// </summary>
+ /// <param name="entity"></param>
+ /// <param name="code">The status code to set the response as</param>
+ /// <param name="filePath">The path of the relative file to send</param>
+ /// <returns>True if the file was found, false if the file does not exist or cannot be accessed</returns>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ContentTypeUnacceptableException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool CloseWithRelativeFile(this HttpEntity entity, HttpStatusCode code, string filePath)
+ {
+ //See if file exists and is within the root's directory
+ if (entity.RequestedRoot.FindResourceInRoot(filePath, out string realPath))
+ {
+ //get file-info
+ FileInfo realFile = new(realPath);
+ //Close the response with the file stream
+ entity.CloseResponse(code, realFile);
+ return true;
+ }
+ return false;
+ }
+
+ /// <summary>
+ /// Redirects a client using the specified <see cref="RedirectType"/>
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="type">The <see cref="RedirectType"/> redirection type</param>
+ /// <param name="location">Location to direct client to, sets the "Location" header</param>
+ /// <remarks>Sets required headers for redirection, disables cache control, and returns the status code to the client</remarks>
+ /// <exception cref="UriFormatException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Redirect(this IHttpEvent ev, RedirectType type, string location)
+ {
+ Redirect(ev, type, new Uri(location, UriKind.RelativeOrAbsolute));
+ }
+
+ /// <summary>
+ /// Redirects a client using the specified <see cref="RedirectType"/>
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="type">The <see cref="RedirectType"/> redirection type</param>
+ /// <param name="location">Location to direct client to, sets the "Location" header</param>
+ /// <remarks>Sets required headers for redirection, disables cache control, and returns the status code to the client</remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Redirect(this IHttpEvent ev, RedirectType type, Uri location)
+ {
+ //Encode the string for propery http url formatting and set the location header
+ ev.Server.Headers[HttpResponseHeader.Location] = location.ToString();
+ ev.Server.SetNoCache();
+ //Set redirect the ressponse redirect code type
+ ev.CloseResponse((HttpStatusCode)type);
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Attempts to read and deserialize a JSON object from the reqeust body (form data or urlencoded)
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="ev"></param>
+ /// <param name="key">Request argument key (name)</param>
+ /// <param name="obj"></param>
+ /// <returns>true if the argument was found and successfully converted to json</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidJsonRequestException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool TryGetJsonFromArg<T>(this IHttpEvent ev, string key, out T? obj) => TryGetJsonFromArg(ev, key, SR_OPTIONS, out obj);
+
+ /// <summary>
+ /// Attempts to read and deserialize a JSON object from the reqeust body (form data or urlencoded)
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="ev"></param>
+ /// <param name="key">Request argument key (name)</param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during deserialization </param>
+ /// <param name="obj"></param>
+ /// <returns>true if the argument was found and successfully converted to json</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidJsonRequestException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool TryGetJsonFromArg<T>(this IHttpEvent ev, string key, JsonSerializerOptions options, out T? obj)
+ {
+ //Check for key in argument
+ if (ev.RequestArgs.TryGetNonEmptyValue(key, out string? value))
+ {
+ try
+ {
+ //Deserialize and return the object
+ obj = value.AsJsonObject<T>(options);
+ return true;
+ }
+ catch(JsonException je)
+ {
+ throw new InvalidJsonRequestException(je);
+ }
+ }
+ obj = default;
+ return false;
+ }
+
+ /// <summary>
+ /// Reads the value stored at the key location in the request body arguments, into a <see cref="JsonDocument"/>
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="key">Request argument key (name)</param>
+ /// <param name="options"><see cref="JsonDocumentOptions"/> to use during parsing</param>
+ /// <returns>A new <see cref="JsonDocument"/> if the key is found, null otherwise</returns>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="InvalidJsonRequestException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static JsonDocument? GetJsonFromArg(this IHttpEvent ev, string key, in JsonDocumentOptions options = default)
+ {
+ try
+ {
+ //Check for key in argument
+ return ev.RequestArgs.TryGetNonEmptyValue(key, out string? value) ? JsonDocument.Parse(value, options) : null;
+ }
+ catch (JsonException je)
+ {
+ throw new InvalidJsonRequestException(je);
+ }
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) and the file is <see cref="ContentType.Json"/>
+ /// file. It will be deserialzied to the specified object
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="ev"></param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during deserialization </param>
+ /// <returns>Returns the deserialized object if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidJsonRequestException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static T? GetJsonFromFile<T>(this IHttpEvent ev, JsonSerializerOptions? options = null, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return default;
+ }
+
+ FileUpload file = ev.Files[uploadIndex];
+ //Make sure the file is a json file
+ if (file.ContentType != ContentType.Json)
+ {
+ return default;
+ }
+ try
+ {
+ //Beware this will buffer the entire file object before it attmepts to de-serialize it
+ return VnEncoding.JSONDeserializeFromBinary<T>(file.FileData, options);
+ }
+ catch (JsonException je)
+ {
+ throw new InvalidJsonRequestException(je);
+ }
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) and the file is <see cref="ContentType.Json"/>
+ /// file. It will be parsed into a new <see cref="JsonDocument"/>
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <returns>Returns the parsed <see cref="JsonDocument"/>if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidJsonRequestException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static JsonDocument? GetJsonFromFile(this IHttpEvent ev, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return default;
+ }
+ FileUpload file = ev.Files[uploadIndex];
+ //Make sure the file is a json file
+ if (file.ContentType != ContentType.Json)
+ {
+ return default;
+ }
+ try
+ {
+ return JsonDocument.Parse(file.FileData);
+ }
+ catch(JsonException je)
+ {
+ throw new InvalidJsonRequestException(je);
+ }
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) and the file is <see cref="ContentType.Json"/>
+ /// file. It will be deserialzied to the specified object
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="ev"></param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during deserialization </param>
+ /// <returns>The deserialized object if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ /// <exception cref="InvalidJsonRequestException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ValueTask<T?> GetJsonFromFileAsync<T>(this HttpEntity ev, JsonSerializerOptions? options = null, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return ValueTask.FromResult<T?>(default);
+ }
+ FileUpload file = ev.Files[uploadIndex];
+ //Make sure the file is a json file
+ if (file.ContentType != ContentType.Json)
+ {
+ return ValueTask.FromResult<T?>(default);
+ }
+ //avoid copying the ev struct, so return deserialze task
+ static async ValueTask<T?> Deserialze(Stream data, JsonSerializerOptions? options, CancellationToken token)
+ {
+ try
+ {
+ //Beware this will buffer the entire file object before it attmepts to de-serialize it
+ return await VnEncoding.JSONDeserializeFromBinaryAsync<T?>(data, options, token);
+ }
+ catch (JsonException je)
+ {
+ throw new InvalidJsonRequestException(je);
+ }
+ }
+ return Deserialze(file.FileData, options, ev.EventCancellation);
+ }
+
+ static readonly Task<JsonDocument?> DocTaskDefault = Task.FromResult<JsonDocument?>(null);
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) and the file is <see cref="ContentType.Json"/>
+ /// file. It will be parsed into a new <see cref="JsonDocument"/>
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <returns>Returns the parsed <see cref="JsonDocument"/>if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Task<JsonDocument?> GetJsonFromFileAsync(this HttpEntity ev, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return DocTaskDefault;
+ }
+ FileUpload file = ev.Files[uploadIndex];
+ //Make sure the file is a json file
+ if (file.ContentType != ContentType.Json)
+ {
+ return DocTaskDefault;
+ }
+ static async Task<JsonDocument?> Deserialze(Stream data, CancellationToken token)
+ {
+ try
+ {
+ //Beware this will buffer the entire file object before it attmepts to de-serialize it
+ return await JsonDocument.ParseAsync(data, cancellationToken: token);
+ }
+ catch (JsonException je)
+ {
+ throw new InvalidJsonRequestException(je);
+ }
+ }
+ return Deserialze(file.FileData, ev.EventCancellation);
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) the specified parser will be called to parse the
+ /// content body asynchronously into a .net object or its default if no attachments are included
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="parser">A function to asynchronously parse the entity body into its object representation</param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <returns>Returns the parsed <typeparamref name="T"/> if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ public static Task<T?> ParseFileAsAsync<T>(this IHttpEvent ev, Func<Stream, Task<T?>> parser, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return Task.FromResult<T?>(default);
+ }
+ //Get the file
+ FileUpload file = ev.Files[uploadIndex];
+ return parser(file.FileData);
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) the specified parser will be called to parse the
+ /// content body asynchronously into a .net object or its default if no attachments are included
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="parser">A function to asynchronously parse the entity body into its object representation</param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <returns>Returns the parsed <typeparamref name="T"/> if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ public static Task<T?> ParseFileAsAsync<T>(this IHttpEvent ev, Func<Stream, string, Task<T?>> parser, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return Task.FromResult<T?>(default);
+ }
+ //Get the file
+ FileUpload file = ev.Files[uploadIndex];
+ //Parse the file using the specified parser
+ return parser(file.FileData, file.ContentTypeString());
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) the specified parser will be called to parse the
+ /// content body asynchronously into a .net object or its default if no attachments are included
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="parser">A function to asynchronously parse the entity body into its object representation</param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <returns>Returns the parsed <typeparamref name="T"/> if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ public static ValueTask<T?> ParseFileAsAsync<T>(this IHttpEvent ev, Func<Stream, ValueTask<T?>> parser, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return ValueTask.FromResult<T?>(default);
+ }
+ //Get the file
+ FileUpload file = ev.Files[uploadIndex];
+ return parser(file.FileData);
+ }
+
+ /// <summary>
+ /// If there are file attachements (form data files or content body) the specified parser will be called to parse the
+ /// content body asynchronously into a .net object or its default if no attachments are included
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="parser">A function to asynchronously parse the entity body into its object representation</param>
+ /// <param name="uploadIndex">The index within <see cref="HttpEntity.Files"/></param> list of the file to read
+ /// <returns>Returns the parsed <typeparamref name="T"/> if found, default otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ public static ValueTask<T?> ParseFileAsAsync<T>(this IHttpEvent ev, Func<Stream, string, ValueTask<T?>> parser, int uploadIndex = 0)
+ {
+ if (ev.Files.Count <= uploadIndex)
+ {
+ return ValueTask.FromResult<T?>(default);
+ }
+ //Get the file
+ FileUpload file = ev.Files[uploadIndex];
+ //Parse the file using the specified parser
+ return parser(file.FileData, file.ContentTypeString());
+ }
+
+ /// <summary>
+ /// Gets the bearer token from an authorization header
+ /// </summary>
+ /// <param name="ci"></param>
+ /// <param name="token">The token stored in the user's authorization header</param>
+ /// <returns>True if the authorization header was set, has a Bearer token value</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool HasAuthorization(this IConnectionInfo ci, [NotNullWhen(true)] out string? token)
+ {
+ //Get auth header value
+ string? authorization = ci.Headers[HttpRequestHeader.Authorization];
+ //Check if its set
+ if (!string.IsNullOrWhiteSpace(authorization))
+ {
+ int bearerIndex = authorization.IndexOf(BEARER_STRING, StringComparison.OrdinalIgnoreCase);
+ //Calc token offset, get token, and trim any whitespace
+ token = authorization[(bearerIndex + BEARER_LEN)..].Trim();
+ return true;
+ }
+ token = null;
+ return false;
+ }
+
+ /// <summary>
+ /// Get a <see cref="DirectoryInfo"/> instance that points to the current sites filesystem root.
+ /// </summary>
+ /// <returns></returns>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="PathTooLongException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static DirectoryInfo GetRootDir(this HttpEntity ev) => new(ev.RequestedRoot.Directory);
+
+ /// <summary>
+ /// Returns the MIME string representation of the content type of the uploaded file.
+ /// </summary>
+ /// <param name="upload"></param>
+ /// <returns>The MIME string representation of the content type of the uploaded file.</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string ContentTypeString(this in FileUpload upload) => HttpHelpers.GetContentTypeString(upload.ContentType);
+
+
+ /// <summary>
+ /// Attemts to upgrade the connection to a websocket, if the setup fails, it sets up the response to the client accordingly.
+ /// </summary>
+ /// <param name="entity"></param>
+ /// <param name="socketOpenedcallback">A delegate that will be invoked when the websocket has been opened by the framework</param>
+ /// <param name="subProtocol">The sub-protocol to use on the current websocket</param>
+ /// <param name="userState">An object to store in the <see cref="WebSocketSession.UserState"/> property when the websocket has been accepted</param>
+ /// <returns>True if operation succeeds.</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ public static bool AcceptWebSocket(this IHttpEvent entity, WebsocketAcceptedCallback socketOpenedcallback, object? userState, string? subProtocol = null)
+ {
+ //Make sure this is a websocket request
+ if (!entity.Server.IsWebSocketRequest)
+ {
+ throw new InvalidOperationException("Connection is not a websocket request");
+ }
+
+ //Must define an accept callback
+ _ = socketOpenedcallback ?? throw new ArgumentNullException(nameof(socketOpenedcallback));
+
+ string? version = entity.Server.Headers["Sec-WebSocket-Version"];
+
+ //rfc6455:4.2, version must equal 13
+ if (!string.IsNullOrWhiteSpace(version) && version.Contains("13", StringComparison.OrdinalIgnoreCase))
+ {
+ //Get socket key
+ string? key = entity.Server.Headers["Sec-WebSocket-Key"];
+ if (!string.IsNullOrWhiteSpace(key) && key.Length < 25)
+ {
+ //Set headers for acceptance
+ entity.Server.Headers[HttpResponseHeader.Upgrade] = "websocket";
+ entity.Server.Headers[HttpResponseHeader.Connection] = "Upgrade";
+
+ //Hash accept string
+ entity.Server.Headers["Sec-WebSocket-Accept"] = ManagedHash.ComputeBase64Hash($"{key.Trim()}{HttpHelpers.WebsocketRFC4122Guid}", HashAlg.SHA1);
+
+ //Protocol if user specified it
+ if (!string.IsNullOrWhiteSpace(subProtocol))
+ {
+ entity.Server.Headers["Sec-WebSocket-Protocol"] = subProtocol;
+ }
+
+ //Setup a new websocket session with a new session id
+ entity.DangerousChangeProtocol(new WebSocketSession(subProtocol, socketOpenedcallback)
+ {
+ IsSecure = entity.Server.IsSecure(),
+ UserState = userState
+ });
+
+ return true;
+ }
+ }
+ //Set the client up for a bad request response, nod a valid websocket request
+ entity.CloseResponse(HttpStatusCode.BadRequest);
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/IJsonSerializerBuffer.cs b/Plugins.Essentials/src/Extensions/IJsonSerializerBuffer.cs
new file mode 100644
index 0000000..34811f4
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/IJsonSerializerBuffer.cs
@@ -0,0 +1,48 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: IJsonSerializerBuffer.cs
+*
+* IJsonSerializerBuffer.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.IO;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ /// <summary>
+ /// Interface for a buffer that can be used to serialize objects to JSON
+ /// </summary>
+ interface IJsonSerializerBuffer
+ {
+ /// <summary>
+ /// Gets a stream used for writing serialzation data to
+ /// </summary>
+ /// <returns>The stream to write JSON data to</returns>
+ Stream GetSerialzingStream();
+
+ /// <summary>
+ /// Called when serialization is complete.
+ /// The stream may be inspected for the serialized data.
+ /// </summary>
+ void SerializationComplete();
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs b/Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs
new file mode 100644
index 0000000..3d441a1
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs
@@ -0,0 +1,100 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: InternalSerializerExtensions.cs
+*
+* InternalSerializerExtensions.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.Text.Json;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+
+ internal static class InternalSerializerExtensions
+ {
+
+ internal static void Serialize<T>(this Utf8JsonWriter writer, IJsonSerializerBuffer buffer, T value, JsonSerializerOptions? options)
+ {
+ //Get stream
+ Stream output = buffer.GetSerialzingStream();
+ try
+ {
+ //Reset writer
+ writer.Reset(output);
+
+ //Serialize
+ JsonSerializer.Serialize(writer, value, options);
+
+ //flush output
+ writer.Flush();
+ }
+ finally
+ {
+ buffer.SerializationComplete();
+ }
+ }
+
+ internal static void Serialize(this Utf8JsonWriter writer, IJsonSerializerBuffer buffer, object value, Type type, JsonSerializerOptions? options)
+ {
+ //Get stream
+ Stream output = buffer.GetSerialzingStream();
+ try
+ {
+ //Reset writer
+ writer.Reset(output);
+
+ //Serialize
+ JsonSerializer.Serialize(writer, value, type, options);
+
+ //flush output
+ writer.Flush();
+ }
+ finally
+ {
+ buffer.SerializationComplete();
+ }
+ }
+
+ internal static void Serialize(this Utf8JsonWriter writer, IJsonSerializerBuffer buffer, JsonDocument document)
+ {
+ //Get stream
+ Stream output = buffer.GetSerialzingStream();
+ try
+ {
+ //Reset writer
+ writer.Reset(output);
+
+ //Serialize
+ document.WriteTo(writer);
+
+ //flush output
+ writer.Flush();
+ }
+ finally
+ {
+ buffer.SerializationComplete();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/InvalidJsonRequestException.cs b/Plugins.Essentials/src/Extensions/InvalidJsonRequestException.cs
new file mode 100644
index 0000000..b2352b2
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/InvalidJsonRequestException.cs
@@ -0,0 +1,57 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: InvalidJsonRequestException.cs
+*
+* InvalidJsonRequestException.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.Text.Json;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ /// <summary>
+ /// Wraps a <see cref="JsonException"/> that is thrown when a JSON request message
+ /// was unsuccessfully parsed.
+ /// </summary>
+ public class InvalidJsonRequestException : JsonException
+ {
+ /// <summary>
+ /// Creates a new <see cref="InvalidJsonRequestException"/> wrapper from a base <see cref="JsonException"/>
+ /// </summary>
+ /// <param name="baseExp"></param>
+ public InvalidJsonRequestException(JsonException baseExp)
+ : base(baseExp.Message, baseExp.Path, baseExp.LineNumber, baseExp.BytePositionInLine, baseExp.InnerException)
+ {
+ base.HelpLink = baseExp.HelpLink;
+ base.Source = baseExp.Source;
+ }
+
+ public InvalidJsonRequestException()
+ {}
+
+ public InvalidJsonRequestException(string message) : base(message)
+ {}
+
+ public InvalidJsonRequestException(string message, System.Exception innerException) : base(message, innerException)
+ {}
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/JsonResponse.cs b/Plugins.Essentials/src/Extensions/JsonResponse.cs
new file mode 100644
index 0000000..22cccd9
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/JsonResponse.cs
@@ -0,0 +1,112 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: JsonResponse.cs
+*
+* JsonResponse.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.Buffers;
+using System.IO;
+
+using VNLib.Net.Http;
+using VNLib.Utils.Extensions;
+using VNLib.Utils.IO;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Memory.Caching;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ internal sealed class JsonResponse : IJsonSerializerBuffer, IMemoryResponseReader
+ {
+ private readonly IObjectRental<JsonResponse> _pool;
+
+ private readonly MemoryHandle<byte> _handle;
+ private readonly IMemoryOwner<byte> _memoryOwner;
+ //Stream "owns" the handle, so we cannot dispose the stream
+ private readonly VnMemoryStream _asStream;
+
+ private int _written;
+
+ internal JsonResponse(IObjectRental<JsonResponse> pool)
+ {
+ _pool = pool;
+
+ //Alloc buffer
+ _handle = Memory.Shared.Alloc<byte>(4096, false);
+ //Consume handle for stream, but make sure not to dispose the stream
+ _asStream = VnMemoryStream.ConsumeHandle(_handle, 0, false);
+ //Get memory owner from handle
+ _memoryOwner = _handle.ToMemoryManager(false);
+ }
+
+ ~JsonResponse()
+ {
+ _handle.Dispose();
+ }
+
+ ///<inheritdoc/>
+ public Stream GetSerialzingStream()
+ {
+ //Reset stream position
+ _asStream.Seek(0, SeekOrigin.Begin);
+ return _asStream;
+ }
+
+ ///<inheritdoc/>
+ public void SerializationComplete()
+ {
+ //Reset written position
+ _written = 0;
+ //Update remaining pointer
+ Remaining = Convert.ToInt32(_asStream.Position);
+ }
+
+
+ ///<inheritdoc/>
+ public int Remaining { get; private set; }
+
+ ///<inheritdoc/>
+ void IMemoryResponseReader.Advance(int written)
+ {
+ //Update position
+ _written += written;
+ Remaining -= written;
+ }
+ ///<inheritdoc/>
+ void IMemoryResponseReader.Close()
+ {
+ //Reset and return to pool
+ _written = 0;
+ Remaining = 0;
+ //Return self back to pool
+ _pool.Return(this);
+ }
+
+ ///<inheritdoc/>
+ ReadOnlyMemory<byte> IMemoryResponseReader.GetMemory()
+ {
+ //Get memory from the memory owner and offet the slice,
+ return _memoryOwner.Memory.Slice(_written, Remaining);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/RedirectType.cs b/Plugins.Essentials/src/Extensions/RedirectType.cs
new file mode 100644
index 0000000..eff4d38
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/RedirectType.cs
@@ -0,0 +1,37 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: RedirectType.cs
+*
+* RedirectType.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.Net;
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ /// <summary>
+ /// Shortened list of <see cref="HttpStatusCode"/>s for redirecting connections
+ /// </summary>
+ public enum RedirectType
+ {
+ None,
+ Moved = 301, Found = 302, SeeOther = 303, Temporary = 307, Permanent = 308
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/SimpleMemoryResponse.cs b/Plugins.Essentials/src/Extensions/SimpleMemoryResponse.cs
new file mode 100644
index 0000000..a0f2b17
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/SimpleMemoryResponse.cs
@@ -0,0 +1,89 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: SimpleMemoryResponse.cs
+*
+* SimpleMemoryResponse.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.Text;
+using VNLib.Net.Http;
+using System.Buffers;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ internal sealed class SimpleMemoryResponse : IMemoryResponseReader
+ {
+ private byte[]? _buffer;
+ private int _written;
+
+ /// <summary>
+ /// Copies the data in the specified buffer to the internal buffer
+ /// to initalize the new <see cref="SimpleMemoryResponse"/>
+ /// </summary>
+ /// <param name="data">The data to copy</param>
+ public SimpleMemoryResponse(ReadOnlySpan<byte> data)
+ {
+ Remaining = data.Length;
+ //Alloc buffer
+ _buffer = ArrayPool<byte>.Shared.Rent(Remaining);
+ //Copy data to buffer
+ data.CopyTo(_buffer);
+ }
+
+ /// <summary>
+ /// Encodes the character buffer data using the encoder and stores
+ /// the result in the internal buffer for reading.
+ /// </summary>
+ /// <param name="data">The data to encode</param>
+ /// <param name="enc">The encoder to use</param>
+ public SimpleMemoryResponse(ReadOnlySpan<char> data, Encoding enc)
+ {
+ //Calc byte count
+ Remaining = enc.GetByteCount(data);
+
+ //Alloc buffer
+ _buffer = ArrayPool<byte>.Shared.Rent(Remaining);
+
+ //Encode data
+ Remaining = enc.GetBytes(data, _buffer);
+ }
+
+ ///<inheritdoc/>
+ public int Remaining { get; private set; }
+ ///<inheritdoc/>
+ void IMemoryResponseReader.Advance(int written)
+ {
+ Remaining -= written;
+ _written += written;
+ }
+ ///<inheritdoc/>
+ void IMemoryResponseReader.Close()
+ {
+ //Return buffer to pool
+ ArrayPool<byte>.Shared.Return(_buffer!);
+ _buffer = null;
+ }
+ ///<inheritdoc/>
+ ReadOnlyMemory<byte> IMemoryResponseReader.GetMemory() => _buffer!.AsMemory(_written, Remaining);
+ }
+} \ No newline at end of file
diff --git a/Plugins.Essentials/src/Extensions/UserExtensions.cs b/Plugins.Essentials/src/Extensions/UserExtensions.cs
new file mode 100644
index 0000000..9223b1d
--- /dev/null
+++ b/Plugins.Essentials/src/Extensions/UserExtensions.cs
@@ -0,0 +1,94 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: UserExtensions.cs
+*
+* UserExtensions.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.Text.Json;
+
+using VNLib.Plugins.Essentials.Users;
+using VNLib.Plugins.Essentials.Accounts;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Extensions
+{
+ /// <summary>
+ /// Provides extension methods to the Users namespace
+ /// </summary>
+ public static class UserExtensions
+ {
+
+ private const string PROFILE_ENTRY = "__.prof";
+
+ /// <summary>
+ /// Stores the user's profile to their entry.
+ /// <br/>
+ /// NOTE: You must validate/filter data before storing
+ /// </summary>
+ /// <param name="ud"></param>
+ /// <param name="profile">The profile object to store on account</param>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static void SetProfile(this IUser ud, AccountData? profile)
+ {
+ //Clear entry if its null
+ if (profile == null)
+ {
+ ud[PROFILE_ENTRY] = null!;
+ return;
+ }
+ //Dont store duplicate values
+ profile.Created = null;
+ profile.EmailAddress = null;
+ ud.SetObject(PROFILE_ENTRY, profile);
+ }
+ /// <summary>
+ /// Stores the serialized string user's profile to their entry.
+ /// <br/>
+ /// NOTE: No data validation checks are performed
+ /// </summary>
+ /// <param name="ud"></param>
+ /// <param name="jsonProfile">The JSON serialized "raw" profile data</param>
+ public static void SetProfile(this IUser ud, string jsonProfile) => ud[PROFILE_ENTRY] = jsonProfile;
+ /// <summary>
+ /// Recovers the user's stored profile
+ /// </summary>
+ /// <returns>The user's profile stored in the entry or null if no entry is found</returns>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static AccountData? GetProfile(this IUser ud)
+ {
+ //Recover profile data, or create new empty profile data
+ AccountData? ad = ud.GetObject<AccountData>(PROFILE_ENTRY);
+ if (ad == null)
+ {
+ return null;
+ }
+ //Set email the same as the account
+ ad.EmailAddress = ud.EmailAddress;
+ //Store the rfc time
+ ad.Created = ud.Created.ToString("R");
+ return ad;
+ }
+ }
+} \ No newline at end of file