diff options
Diffstat (limited to 'Plugins.Essentials/src/Extensions')
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 |