diff options
Diffstat (limited to 'Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs')
-rw-r--r-- | Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs | 848 |
1 files changed, 848 insertions, 0 deletions
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 |