aboutsummaryrefslogtreecommitdiff
path: root/lib/Plugins.Essentials/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Plugins.Essentials/src')
-rw-r--r--lib/Plugins.Essentials/src/EventProcessor.cs21
-rw-r--r--lib/Plugins.Essentials/src/EventProcessorConfig.cs6
-rw-r--r--lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs247
-rw-r--r--lib/Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs62
-rw-r--r--lib/Plugins.Essentials/src/Extensions/JsonResponse.cs114
-rw-r--r--lib/Plugins.Essentials/src/FilePathCache.cs123
-rw-r--r--lib/Plugins.Essentials/src/HttpEntity.cs88
-rw-r--r--lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs18
-rw-r--r--lib/Plugins.Essentials/src/Sessions/SessionBase.cs2
-rw-r--r--lib/Plugins.Essentials/src/Sessions/SessionInfo.cs36
-rw-r--r--lib/Plugins.Essentials/src/WebSocketSession.cs22
11 files changed, 456 insertions, 283 deletions
diff --git a/lib/Plugins.Essentials/src/EventProcessor.cs b/lib/Plugins.Essentials/src/EventProcessor.cs
index ba7aa3c..908ad07 100644
--- a/lib/Plugins.Essentials/src/EventProcessor.cs
+++ b/lib/Plugins.Essentials/src/EventProcessor.cs
@@ -159,6 +159,8 @@ namespace VNLib.Plugins.Essentials
private readonly MiddlewareController _middleware = new(config);
+ private readonly FilePathCache _pathCache = FilePathCache.GetCacheStore(config.FilePathCacheMaxAge);
+
///<inheritdoc/>
public virtual async ValueTask ClientConnectedAsync(IHttpEvent httpEvent)
{
@@ -620,6 +622,25 @@ namespace VNLib.Plugins.Essentials
/// <returns>True if the resource exists and is allowed to be accessed</returns>
public bool FindResourceInRoot(string resourcePath, out string path)
{
+ //Try to get the translated file path from cache
+ if (_pathCache.TryGetMappedPath(resourcePath, out path))
+ {
+ return true;
+ }
+
+ //Cache miss, force a lookup
+ if (FindFileResourceInternal(resourcePath, out path))
+ {
+ //Store the path in the cache for next lookup
+ _pathCache.StorePathMapping(resourcePath, path);
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool FindFileResourceInternal(string resourcePath, out string path)
+ {
//Check after fully qualified path name because above is a special case
path = TranslateResourcePath(resourcePath);
string extension = Path.GetExtension(path);
diff --git a/lib/Plugins.Essentials/src/EventProcessorConfig.cs b/lib/Plugins.Essentials/src/EventProcessorConfig.cs
index 6e101eb..831f5dc 100644
--- a/lib/Plugins.Essentials/src/EventProcessorConfig.cs
+++ b/lib/Plugins.Essentials/src/EventProcessorConfig.cs
@@ -90,5 +90,11 @@ namespace VNLib.Plugins.Essentials
/// A <see cref="TimeSpan"/> for how long a connection may remain open before all operations are cancelled
/// </summary>
public TimeSpan ExecutionTimeout { get; init; } = TimeSpan.Zero;
+
+ /// <summary>
+ /// Enables or disables the use of the file path cache. If set to zero , the cache will be disabled,
+ /// otherwise sets the maximum amount of time a file path is to be cached.
+ /// </summary>
+ public TimeSpan FilePathCacheMaxAge { get; init; } = TimeSpan.Zero;
}
} \ No newline at end of file
diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
index 0ca5b8f..db30f0f 100644
--- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
+++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
@@ -34,7 +34,7 @@ using System.Runtime.CompilerServices;
using VNLib.Net.Http;
using VNLib.Hashing;
using VNLib.Utils;
-using VNLib.Utils.Memory.Caching;
+using VNLib.Utils.IO;
using static VNLib.Plugins.Essentials.Statics;
namespace VNLib.Plugins.Essentials.Extensions
@@ -44,15 +44,13 @@ namespace VNLib.Plugins.Essentials.Extensions
/// Provides extension methods for manipulating <see cref="HttpEvent"/>s
/// </summary>
public static class EssentialHttpEventExtensions
- {
-
+ {
+ const int JsonInitBufferSize = 4096;
/*
* 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);
+ private static readonly ThreadLocal<Utf8JsonWriter> LocalSerializer = new(() => new(Stream.Null));
#region Response Configuring
@@ -68,7 +66,8 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <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);
+ 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
@@ -85,19 +84,30 @@ namespace VNLib.Plugins.Essentials.Extensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CloseResponseJson<T>(this IHttpEvent ev, HttpStatusCode code, T response, JsonSerializerOptions? options)
{
- JsonResponse rbuf = ResponsePool.Rent();
+ /*
+ * Taking advantage of a new HttpEntity feature that wraps the memory stream
+ * in a reader class that avoids a user-space copy. While the stream and wrapper
+ * must be allocated, it is far safer and easier on the long term process
+ * memory than large static pools the application/user cannot control.
+ */
+
+#pragma warning disable CA2000 // Dispose objects before losing scope
+
+ VnMemoryStream vms = new(JsonInitBufferSize, zero: false);
+
+#pragma warning restore CA2000 // Dispose objects before losing scope
+
try
{
//Serialze the object on the thread local serializer
- LocalSerializer.Value!.Serialize(rbuf, response, options);
+ LocalSerializer.Value!.Serialize(vms, response, options);
//Set the response as the buffer,
- ev.CloseResponse(code, ContentType.Json, rbuf);
+ ev.CloseResponse(code, ContentType.Json, vms, vms.Length);
}
catch
{
- //Return back to pool on error
- ResponsePool.Return(rbuf);
+ vms.Dispose();
throw;
}
}
@@ -114,7 +124,8 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <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);
+ 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
@@ -131,19 +142,29 @@ namespace VNLib.Plugins.Essentials.Extensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CloseResponseJson(this IHttpEvent ev, HttpStatusCode code, object response, Type type, JsonSerializerOptions? options)
{
- JsonResponse rbuf = ResponsePool.Rent();
+ /*
+ * Taking advantage of a new HttpEntity feature that wraps the memory stream
+ * in a reader class that avoids a user-space copy. While the stream and wrapper
+ * must be allocated, it is far safer and easier on the long term process
+ * memory than large static pools the application/user cannot control.
+ */
+#pragma warning disable CA2000 // Dispose objects before losing scope
+
+ VnMemoryStream vms = new(JsonInitBufferSize, zero: false);
+
+#pragma warning restore CA2000 // Dispose objects before losing scope
+
try
{
//Serialze the object on the thread local serializer
- LocalSerializer.Value!.Serialize(rbuf, response, type, options);
+ LocalSerializer.Value!.Serialize(vms, response, type, options);
//Set the response as the buffer,
- ev.CloseResponse(code, ContentType.Json, rbuf);
+ ev.CloseResponse(code, ContentType.Json, vms, vms.Length);
}
catch
{
- //Return back to pool on error
- ResponsePool.Return(rbuf);
+ vms.Dispose();
throw;
}
}
@@ -169,19 +190,30 @@ namespace VNLib.Plugins.Essentials.Extensions
return;
}
- JsonResponse rbuf = ResponsePool.Rent();
+ /*
+ * Taking advantage of a new HttpEntity feature that wraps the memory stream
+ * in a reader class that avoids a user-space copy. While the stream and wrapper
+ * must be allocated, it is far safer and easier on the long term process
+ * memory than large static pools the application/user cannot control.
+ */
+
+#pragma warning disable CA2000 // Dispose objects before losing scope
+
+ VnMemoryStream vms = new(JsonInitBufferSize, zero: false);
+
+#pragma warning restore CA2000 // Dispose objects before losing scope
+
try
{
//Serialze the object on the thread local serializer
- LocalSerializer.Value!.Serialize(rbuf, data);
+ LocalSerializer.Value!.Serialize(vms, data);
//Set the response as the buffer,
- ev.CloseResponse(code, ContentType.Json, rbuf);
+ ev.CloseResponse(code, ContentType.Json, vms, vms.Length);
}
catch
{
- //Return back to pool on error
- ResponsePool.Return(rbuf);
+ vms.Dispose();
throw;
}
}
@@ -320,7 +352,8 @@ namespace VNLib.Plugins.Essentials.Extensions
ArgumentNullException.ThrowIfNull(file);
//Get content type from filename
- ContentType ct = HttpHelpers.GetContentTypeFromFile(file.Name);
+ ContentType ct = HttpHelpers.GetContentTypeFromFile(file.Name);
+
//Set the input as a stream
ev.CloseResponse(code, ct, file, file.Length);
}
@@ -366,10 +399,9 @@ namespace VNLib.Plugins.Essentials.Extensions
ArgumentNullException.ThrowIfNull(encoding, nameof(encoding));
//Get new simple memory response
- IMemoryResponseReader reader = new SimpleMemoryResponse(data, encoding);
- ev.CloseResponse(code, type, reader);
+ ev.CloseResponse(code, type, entity: new SimpleMemoryResponse(data, encoding));
}
-
+
/// <summary>
/// Close a response to a connection by copying the speciifed binary buffer
/// </summary>
@@ -388,13 +420,12 @@ namespace VNLib.Plugins.Essentials.Extensions
if (data.IsEmpty)
{
- ev.CloseResponse(code);
- return;
+ ev.CloseResponse(code);
+ }
+ else
+ {
+ ev.CloseResponse(code, type, entity: new SimpleMemoryResponse(data));
}
-
- //Get new simple memory response
- IMemoryResponseReader reader = new SimpleMemoryResponse(data);
- ev.CloseResponse(code, type, reader);
}
/// <summary>
@@ -414,15 +445,18 @@ namespace VNLib.Plugins.Essentials.Extensions
ArgumentNullException.ThrowIfNull(filePath);
//See if file exists and is within the root's directory
- if (entity.RequestedRoot.FindResourceInRoot(filePath, out string realPath))
+ 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;
}
- return false;
+
+ //get file-info
+ FileInfo realFile = new(realPath);
+
+ //Close the response with the file stream
+ entity.CloseResponse(code, realFile);
+
+ return true;
}
/// <summary>
@@ -435,7 +469,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <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));
+ => Redirect(ev, type, location: new Uri(location, UriKind.RelativeOrAbsolute));
/// <summary>
/// Redirects a client using the specified <see cref="RedirectType"/>
@@ -510,6 +544,7 @@ namespace VNLib.Plugins.Essentials.Extensions
throw new InvalidJsonRequestException(je);
}
}
+
obj = default;
return false;
}
@@ -531,7 +566,9 @@ namespace VNLib.Plugins.Essentials.Extensions
try
{
//Check for key in argument
- return ev.RequestArgs.TryGetNonEmptyValue(key, out string? value) ? JsonDocument.Parse(value, options) : null;
+ return ev.RequestArgs.TryGetNonEmptyValue(key, out string? value)
+ ? JsonDocument.Parse(value, options)
+ : null;
}
catch (JsonException je)
{
@@ -562,11 +599,13 @@ namespace VNLib.Plugins.Essentials.Extensions
}
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
@@ -597,12 +636,15 @@ namespace VNLib.Plugins.Essentials.Extensions
{
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);
@@ -635,12 +677,15 @@ namespace VNLib.Plugins.Essentials.Extensions
{
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)
{
@@ -654,6 +699,7 @@ namespace VNLib.Plugins.Essentials.Extensions
throw new InvalidJsonRequestException(je);
}
}
+
return Deserialze(file.FileData, options, ev.EventCancellation);
}
@@ -678,12 +724,15 @@ namespace VNLib.Plugins.Essentials.Extensions
{
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
@@ -696,6 +745,7 @@ namespace VNLib.Plugins.Essentials.Extensions
throw new InvalidJsonRequestException(je);
}
}
+
return Deserialze(file.FileData, ev.EventCancellation);
}
@@ -718,8 +768,10 @@ namespace VNLib.Plugins.Essentials.Extensions
{
return Task.FromResult<T?>(default);
}
+
//Get the file
FileUpload file = ev.Files[uploadIndex];
+
return parser(file.FileData);
}
@@ -742,8 +794,10 @@ namespace VNLib.Plugins.Essentials.Extensions
{
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());
}
@@ -767,8 +821,10 @@ namespace VNLib.Plugins.Essentials.Extensions
{
return ValueTask.FromResult<T?>(default);
}
+
//Get the file
FileUpload file = ev.Files[uploadIndex];
+
return parser(file.FileData);
}
@@ -876,6 +932,27 @@ namespace VNLib.Plugins.Essentials.Extensions
public static string ContentTypeString(this in FileUpload upload) => HttpHelpers.GetContentTypeString(upload.ContentType);
/// <summary>
+ /// Copies the contents of the uploaded file to the specified stream asynchronously
+ /// </summary>
+ /// <param name="upload"></param>
+ /// <param name="bufferSize">The size of the buffer to use when copying stream data</param>
+ /// <param name="outputStream">The stream to write the file data to</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns></returns>
+ public static Task CopyToAsync(this in FileUpload upload, Stream outputStream, int bufferSize, CancellationToken cancellation = default)
+ => upload.FileData.CopyToAsync(outputStream, bufferSize, cancellation);
+
+ /// <summary>
+ /// Copies the contents of the uploaded file to the specified stream asynchronously
+ /// </summary>
+ /// <param name="upload"></param>
+ /// <param name="outputStream">The stream to write the file data to</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>A task that resolves when the copy operation has completed</returns>
+ public static Task CopyToAsync(this in FileUpload upload, Stream outputStream, CancellationToken cancellation = default)
+ => upload.FileData.CopyToAsync(outputStream, cancellation);
+
+ /// <summary>
/// Sets the <see cref="HttpControlMask.CompressionDisabled"/> flag on the current
/// <see cref="IHttpEvent"/> instance to disable dynamic compression on the response.
/// </summary>
@@ -903,30 +980,31 @@ namespace VNLib.Plugins.Essentials.Extensions
{
//Must define an accept callback
ArgumentNullException.ThrowIfNull(socketOpenedCallback);
-
- if (PrepWebSocket(entity, subProtocol))
+
+ if (!PrepWebSocket(entity, subProtocol))
{
- //Set a default keep alive if none was specified
- if (keepAlive == default)
- {
- keepAlive = TimeSpan.FromSeconds(30);
- }
+ return false;
+ }
- IAlternateProtocol ws = new WebSocketSession<T>(GetNewSocketId(), socketOpenedCallback)
- {
- SubProtocol = subProtocol,
- IsSecure = entity.Server.IsSecure(),
- UserState = userState,
- KeepAlive = keepAlive,
- };
+ //Set a default keep alive if none was specified
+ if (keepAlive == default)
+ {
+ keepAlive = TimeSpan.FromSeconds(30);
+ }
- //Setup a new websocket session with a new session id
- entity.DangerousChangeProtocol(ws);
+ IAlternateProtocol ws = new WebSocketSession<T>(socketOpenedCallback)
+ {
+ SocketID = GetNewSocketId(),
+ SubProtocol = subProtocol,
+ IsSecure = entity.Server.IsSecure(),
+ UserState = userState,
+ KeepAlive = keepAlive,
+ };
- return true;
- }
+ //Setup a new websocket session with a new session id
+ entity.DangerousChangeProtocol(ws);
- return false;
+ return true;
}
/// <summary>
@@ -939,39 +1017,45 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <returns>True if operation succeeds.</returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InvalidOperationException"></exception>
- public static bool AcceptWebSocket(this IHttpEvent entity, WebSocketAcceptedCallback socketOpenedCallback, string? subProtocol = null, TimeSpan keepAlive = default)
+ public static bool AcceptWebSocket(
+ this IHttpEvent entity,
+ WebSocketAcceptedCallback socketOpenedCallback,
+ string? subProtocol = null,
+ TimeSpan keepAlive = default
+ )
{
//Must define an accept callback
ArgumentNullException.ThrowIfNull(entity);
- ArgumentNullException.ThrowIfNull(socketOpenedCallback);
+ ArgumentNullException.ThrowIfNull(socketOpenedCallback);
- if(PrepWebSocket(entity, subProtocol))
+ if (!PrepWebSocket(entity, subProtocol))
{
- //Set a default keep alive if none was specified
- if (keepAlive == default)
- {
- keepAlive = TimeSpan.FromSeconds(30);
- }
+ return false;
+ }
- IAlternateProtocol ws = new WebSocketSession(GetNewSocketId(), socketOpenedCallback)
- {
- SubProtocol = subProtocol,
- IsSecure = entity.Server.IsSecure(),
- KeepAlive = keepAlive,
- };
+ //Set a default keep alive if none was specified
+ if (keepAlive == default)
+ {
+ keepAlive = TimeSpan.FromSeconds(30);
+ }
- //Setup a new websocket session with a new session id
- entity.DangerousChangeProtocol(ws);
+ IAlternateProtocol ws = new WebSocketSession(socketOpenedCallback)
+ {
+ SocketID = GetNewSocketId(),
+ SubProtocol = subProtocol,
+ IsSecure = entity.Server.IsSecure(),
+ KeepAlive = keepAlive,
+ };
- return true;
- }
+ //Setup a new websocket session with a new session id
+ entity.DangerousChangeProtocol(ws);
- return false;
+ return true;
}
private static string GetNewSocketId() => Guid.NewGuid().ToString("N");
- private static bool PrepWebSocket(this IHttpEvent entity, string? subProtocol = null)
+ private static bool PrepWebSocket(this IHttpEvent entity, string? subProtocol)
{
ArgumentNullException.ThrowIfNull(entity);
@@ -1006,6 +1090,7 @@ namespace VNLib.Plugins.Essentials.Extensions
return true;
}
}
+
//Set the client up for a bad request response, nod a valid websocket request
entity.CloseResponse(HttpStatusCode.BadRequest);
return false;
diff --git a/lib/Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs b/lib/Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs
index 817b673..3a77541 100644
--- a/lib/Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs
+++ b/lib/Plugins.Essentials/src/Extensions/InternalSerializerExtensions.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -23,75 +23,71 @@
*/
using System;
-using System.IO;
using System.Text.Json;
+using VNLib.Utils.IO;
+
namespace VNLib.Plugins.Essentials.Extensions
{
internal static class InternalSerializerExtensions
{
- internal static void Serialize<T>(this Utf8JsonWriter writer, IJsonSerializerBuffer buffer, T value, JsonSerializerOptions? options)
+ internal static void Serialize<T>(this Utf8JsonWriter writer, VnMemoryStream buffer, T value, JsonSerializerOptions? options)
{
- //Get stream
- Stream output = buffer.GetSerialzingStream();
try
{
- //Reset writer
- writer.Reset(output);
-
- //Serialize
+ //Reset and init the output stream
+ writer.Reset(buffer);
+
JsonSerializer.Serialize(writer, value, options);
-
- //flush output
+
writer.Flush();
+
+ buffer.Seek(0, System.IO.SeekOrigin.Begin);
}
finally
{
- buffer.SerializationComplete();
+ writer.Reset();
}
}
- internal static void Serialize(this Utf8JsonWriter writer, IJsonSerializerBuffer buffer, object value, Type type, JsonSerializerOptions? options)
+ internal static void Serialize(this Utf8JsonWriter writer, VnMemoryStream 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
+ //Reset and init the output stream
+ writer.Reset(buffer);
+
+ JsonSerializer.Serialize(writer, value, options);
+
writer.Flush();
+
+ buffer.Seek(0, System.IO.SeekOrigin.Begin);
}
finally
{
- buffer.SerializationComplete();
+ writer.Reset();
}
}
- internal static void Serialize(this Utf8JsonWriter writer, IJsonSerializerBuffer buffer, JsonDocument document)
- {
- //Get stream
- Stream output = buffer.GetSerialzingStream();
+ internal static void Serialize(this Utf8JsonWriter writer, VnMemoryStream buffer, JsonDocument document)
+ {
try
{
- //Reset writer
- writer.Reset(output);
-
- //Serialize
+ //Reset and init the output stream
+ writer.Reset(buffer);
+
document.WriteTo(writer);
- //flush output
writer.Flush();
+
+ buffer.Seek(0, System.IO.SeekOrigin.Begin);
}
finally
{
- buffer.SerializationComplete();
+ writer.Reset();
+ ;
}
}
}
diff --git a/lib/Plugins.Essentials/src/Extensions/JsonResponse.cs b/lib/Plugins.Essentials/src/Extensions/JsonResponse.cs
deleted file mode 100644
index b418b6f..0000000
--- a/lib/Plugins.Essentials/src/Extensions/JsonResponse.cs
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
-* Copyright (c) 2023 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.IO;
-using System.Buffers;
-
-using VNLib.Net.Http;
-using VNLib.Utils.IO;
-using VNLib.Utils.Memory;
-using VNLib.Utils.Extensions;
-using VNLib.Utils.Memory.Caching;
-
-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)
- {
- /*
- * I am breaking the memoryhandle rules by referrencing the same
- * memory handle in two different wrappers.
- */
-
- _pool = pool;
-
- //Alloc buffer
- _handle = MemoryUtil.Shared.Alloc<byte>(4096, false);
-
- //Create stream around handle and not own it
- _asStream = VnMemoryStream.FromHandle(_handle, false, 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() => _memoryOwner.Memory.Slice(_written, Remaining);
- }
-} \ No newline at end of file
diff --git a/lib/Plugins.Essentials/src/FilePathCache.cs b/lib/Plugins.Essentials/src/FilePathCache.cs
new file mode 100644
index 0000000..d8f414c
--- /dev/null
+++ b/lib/Plugins.Essentials/src/FilePathCache.cs
@@ -0,0 +1,123 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials
+* File: FilePathCache.cs
+*
+* FilePathCache.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;
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+
+namespace VNLib.Plugins.Essentials
+{
+
+ /// <summary>
+ /// Represents a cache store for translated file paths to avoid
+ /// path probing and file system syscalls
+ /// </summary>
+ internal abstract class FilePathCache
+ {
+
+ public abstract bool TryGetMappedPath(string filePath, [NotNullWhen(true)] out string? cachedPath);
+
+ /// <summary>
+ /// Attempts to store a path mapping in the cache store
+ /// </summary>
+ /// <param name="requestPath">The requested input path</param>
+ /// <param name="filePath">The filesystem path this requested path maps to</param>
+ public abstract void StorePathMapping(string requestPath, string filePath);
+
+ /// <summary>
+ /// Creates a new cache store with the specified max age. If max age is zero, the
+ /// cache store will be disabled.
+ /// </summary>
+ /// <param name="maxAge">The max time to store the cahced path reecord</param>
+ /// <returns>The cache store</returns>
+ public static FilePathCache GetCacheStore(TimeSpan maxAge)
+ {
+ return maxAge == TimeSpan.Zero
+ ? new DisabledCacheStore()
+ : new DictBackedFilePathCache(maxAge);
+ }
+
+ /*
+ * A very basic dictionary cache that stores translated paths
+ * from a request input path to a filesystem path.
+ *
+ * This must be thread safe as it's called in a multithreaded context.
+ */
+ private sealed class DictBackedFilePathCache(TimeSpan maxAge) : FilePathCache
+ {
+ private readonly ConcurrentDictionary<string, CachedPath> _pathCache = new(StringComparer.OrdinalIgnoreCase);
+
+ ///<inheritdoc/>
+ public override bool TryGetMappedPath(string filePath, [NotNullWhen(true)] out string? cachedPath)
+ {
+ if (_pathCache.TryGetValue(filePath, out CachedPath cp))
+ {
+ //TODO: Implement a cache eviction policy
+ cachedPath = cp.Path;
+ return true;
+ }
+
+ cachedPath = null;
+ return false;
+ }
+
+ ///<inheritdoc/>
+ public override void StorePathMapping(string requestPath, string filePath)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(requestPath);
+
+ //Cache path is an internal assignment. Should never be null
+ Debug.Assert(filePath is not null);
+
+ //TODO: Implement a cache eviction policy
+ _pathCache[requestPath] = new CachedPath { Path = filePath, LastStored = DateTime.MinValue.Ticks };
+ }
+
+ private struct CachedPath
+ {
+ public required string Path;
+ public required long LastStored;
+ }
+ }
+
+ /*
+ * A cache store that does nothing, it always misses and will
+ * cause a normal file fetch
+ */
+ private sealed class DisabledCacheStore : FilePathCache
+ {
+ ///<inheritdoc/>
+ public override void StorePathMapping(string requestPath, string filePath)
+ { }
+
+ ///<inheritdoc/>
+ public override bool TryGetMappedPath(string filePath, [NotNullWhen(true)] out string? cachedPath)
+ {
+ cachedPath = null;
+ return false;
+ }
+ }
+ }
+}
diff --git a/lib/Plugins.Essentials/src/HttpEntity.cs b/lib/Plugins.Essentials/src/HttpEntity.cs
index ff728e3..86f9924 100644
--- a/lib/Plugins.Essentials/src/HttpEntity.cs
+++ b/lib/Plugins.Essentials/src/HttpEntity.cs
@@ -30,7 +30,9 @@ using System.Diagnostics;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
+using VNLib.Utils.IO;
using VNLib.Net.Http;
+using VNLib.Plugins.Essentials.Content;
using VNLib.Plugins.Essentials.Sessions;
using VNLib.Plugins.Essentials.Extensions;
@@ -214,13 +216,53 @@ namespace VNLib.Plugins.Essentials
* Stream length also should not cause an integer overflow,
* which also mean position is assumed not to overflow
* or cause an overflow during reading
+ *
+ * Finally not all memory streams allow fetching the internal
+ * buffer, so check that it can be aquired.
*/
- if(stream is MemoryStream ms && length < int.MaxValue)
+ if (stream is MemoryStream ms
+ && length < int.MaxValue
+ && ms.TryGetBuffer(out ArraySegment<byte> arrSeg)
+ )
{
Entity.CloseResponse(
code,
type,
- new MemStreamWrapper(ms, (int)length)
+ entity: new MemStreamWrapper(in arrSeg, ms, (int)length)
+ );
+
+ return;
+ }
+
+ /*
+ * Readonly vn streams can also use a shortcut to avoid http buffer allocation and
+ * async streaming. This is done by wrapping the stream in a memory response reader
+ *
+ * Allocating a memory manager requires that the stream is readonly
+ */
+ if (stream is VnMemoryStream vms && length < int.MaxValue)
+ {
+ Entity.CloseResponse(
+ code,
+ type,
+ entity: new VnStreamWrapper(vms, (int)length)
+ );
+
+ return;
+ }
+
+ /*
+ * Files can have a bit more performance using the RandomAccess library when reading
+ * sequential segments without buffering. It avoids a user-space copy and async reading
+ * performance without the file being opened as async.
+ */
+ if(stream is FileStream fs)
+ {
+ Entity.CloseResponse(
+ code,
+ type,
+ entity: new DirectFileStream(fs.SafeFileHandle),
+ length
);
return;
@@ -270,8 +312,11 @@ namespace VNLib.Plugins.Essentials
void IHttpEvent.DangerousChangeProtocol(IAlternateProtocol protocolHandler) => Entity.DangerousChangeProtocol(protocolHandler);
- private sealed class MemStreamWrapper(MemoryStream memStream, int length) : IMemoryResponseReader
+ private sealed class VnStreamWrapper(VnMemoryStream memStream, int length) : IMemoryResponseReader
{
+ //Store memory buffer, causes an internal allocation, so avoid calling mutliple times
+ readonly ReadOnlyMemory<byte> _memory = memStream.AsMemory();
+
readonly int length = length;
/*
@@ -280,6 +325,7 @@ namespace VNLib.Plugins.Essentials
*/
int read = (int)memStream.Position;
+ ///<inheritdoc/>
public int Remaining
{
get
@@ -289,16 +335,46 @@ namespace VNLib.Plugins.Essentials
}
}
+ ///<inheritdoc/>
public void Advance(int written) => read += written;
///<inheritdoc/>
public void Close() => memStream.Dispose();
- public ReadOnlyMemory<byte> GetMemory()
+ ///<inheritdoc/>
+ public ReadOnlyMemory<byte> GetMemory() => _memory.Slice(read, Remaining);
+ }
+
+
+ private sealed class MemStreamWrapper(ref readonly ArraySegment<byte> data, MemoryStream stream, int length) : IMemoryResponseReader
+ {
+ readonly ArraySegment<byte> _data = data;
+ readonly int length = length;
+
+ /*
+ * Stream may be offset by the caller, it needs
+ * to be respected during streaming.
+ */
+ int read = (int)stream.Position;
+
+ ///<inheritdoc/>
+ public int Remaining
{
- byte[] intBuffer = memStream.GetBuffer();
- return new ReadOnlyMemory<byte>(intBuffer, read, Remaining);
+ get
+ {
+ Debug.Assert(length - read >= 0);
+ return length - read;
+ }
}
+
+ ///<inheritdoc/>
+ public void Advance(int written) => read += written;
+
+ ///<inheritdoc/>
+ public void Close() => stream.Dispose();
+
+ ///<inheritdoc/>
+ public ReadOnlyMemory<byte> GetMemory() => _data.AsMemory(read, Remaining);
}
}
}
diff --git a/lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs b/lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs
index 05d6712..bff32e5 100644
--- a/lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs
+++ b/lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs
@@ -78,10 +78,26 @@ namespace VNLib.Plugins.Essentials.Sessions
public static void InitNewSession(this ISession session, IConnectionInfo ci)
{
session.IsCrossOrigin(ci.CrossOrigin);
- session.SetOrigin(ci.Origin?.ToString());
session.SetRefer(ci.Referer?.ToString());
session.SetSecurityProtocol(ci.GetSslProtocol());
session.SetUserAgent(ci.UserAgent);
+
+ /*
+ * If no origin is specified, then we can use the authority of
+ * our current virtual host because it cannot be a cross-origin
+ * request.
+ */
+ if(ci.Origin is null)
+ {
+ string scheme = ci.RequestUri.Scheme;
+ string authority = ci.RequestUri.Authority;
+
+ session.SetOrigin($"{scheme}{authority}");
+ }
+ else
+ {
+ session.SetOrigin(ci.Origin.ToString());
+ }
}
}
diff --git a/lib/Plugins.Essentials/src/Sessions/SessionBase.cs b/lib/Plugins.Essentials/src/Sessions/SessionBase.cs
index 47079a0..5f9b25d 100644
--- a/lib/Plugins.Essentials/src/Sessions/SessionBase.cs
+++ b/lib/Plugins.Essentials/src/Sessions/SessionBase.cs
@@ -87,7 +87,7 @@ namespace VNLib.Plugins.Essentials.Sessions
get
{
//try to parse the IP address, otherwise return null
- _ = IPAddress.TryParse(this[IP_ADDRESS_ENTRY], out IPAddress? ip);
+ _ = IPAddress.TryParse(this[IP_ADDRESS_ENTRY], out IPAddress ip);
return ip;
}
protected set
diff --git a/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs b/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs
index 2edb30c..eccfdfc 100644
--- a/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs
+++ b/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs
@@ -65,9 +65,7 @@ namespace VNLib.Plugins.Essentials.Sessions
{
None = 0x00,
IsSet = 0x01,
- IpMatch = 0x02,
- IsCrossOrigin = 0x04,
- CrossOriginMatch = 0x08,
+ IpMatch = 0x02
}
private readonly ISession UserSession;
@@ -117,24 +115,6 @@ namespace VNLib.Plugins.Essentials.Sessions
}
/// <summary>
- /// If the current connection and stored session have matching cross origin domains
- /// </summary>
- public readonly bool CrossOriginMatch
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _flags.HasFlag(SessionFlags.CrossOriginMatch);
- }
-
- /// <summary>
- /// Was the original session cross origin?
- /// </summary>
- public readonly bool CrossOrigin
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _flags.HasFlag(SessionFlags.IsCrossOrigin);
- }
-
- /// <summary>
/// Was this session just created on this connection?
/// </summary>
public readonly bool IsNew
@@ -252,10 +232,10 @@ namespace VNLib.Plugins.Essentials.Sessions
{
UserSession = session;
- SessionFlags flags = SessionFlags.IsSet;
+ _flags |= SessionFlags.IsSet;
//Set ip match flag if current ip and stored ip match
- flags |= trueIp.Equals(session.UserIP) ? SessionFlags.IpMatch : SessionFlags.None;
+ _flags |= trueIp.Equals(session.UserIP) ? SessionFlags.IpMatch : SessionFlags.None;
//If the session is new, we can store intial security variables
if (session.IsNew)
@@ -266,8 +246,6 @@ namespace VNLib.Plugins.Essentials.Sessions
UserAgent = ci.UserAgent;
SpecifiedOrigin = ci.Origin;
SecurityProcol = ci.GetSslProtocol();
-
- flags |= ci.CrossOrigin ? SessionFlags.IsCrossOrigin : SessionFlags.None;
}
else
{
@@ -275,15 +253,7 @@ namespace VNLib.Plugins.Essentials.Sessions
UserAgent = session.GetUserAgent();
SpecifiedOrigin = session.GetOriginUri();
SecurityProcol = session.GetSecurityProtocol();
-
- flags |= session.IsCrossOrigin() ? SessionFlags.IsCrossOrigin : SessionFlags.None;
}
-
- //Set cross origin orign match flags, if the stored origin, and connection origin
- flags |= ci.Origin != null && ci.Origin.Equals(SpecifiedOrigin) ? SessionFlags.CrossOriginMatch : SessionFlags.None;
-
- //store flags
- _flags = flags;
}
///<inheritdoc/>
diff --git a/lib/Plugins.Essentials/src/WebSocketSession.cs b/lib/Plugins.Essentials/src/WebSocketSession.cs
index e39f352..6c77003 100644
--- a/lib/Plugins.Essentials/src/WebSocketSession.cs
+++ b/lib/Plugins.Essentials/src/WebSocketSession.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -62,10 +62,10 @@ namespace VNLib.Plugins.Essentials
/// connection context and the underlying transport. This session is managed by the parent
/// <see cref="HttpServer"/> that it was created on.
/// </summary>
- public class WebSocketSession : AlternateProtocolBase
+ public class WebSocketSession(WebSocketAcceptedCallback callback) : AlternateProtocolBase
{
internal WebSocket? WsHandle;
- internal readonly WebSocketAcceptedCallback AcceptedCallback;
+ internal readonly WebSocketAcceptedCallback AcceptedCallback = callback;
/// <summary>
/// A cancellation token that can be monitored to reflect the state
@@ -76,7 +76,7 @@ namespace VNLib.Plugins.Essentials
/// <summary>
/// Id assigned to this instance on creation
/// </summary>
- public string SocketID { get; }
+ public required string SocketID { get; init; }
/// <summary>
/// Negotiated sub-protocol
@@ -87,13 +87,6 @@ namespace VNLib.Plugins.Essentials
/// The websocket keep-alive interval
/// </summary>
internal TimeSpan KeepAlive { get; init; }
-
- internal WebSocketSession(string socketId, WebSocketAcceptedCallback callback)
- {
- SocketID = socketId;
- //Store the callback function
- AcceptedCallback = callback;
- }
/// <summary>
/// Initialzes the created websocket with the specified protocol
@@ -175,7 +168,8 @@ namespace VNLib.Plugins.Essentials
/// <param name="flags">Websocket message flags</param>
/// <returns></returns>
/// <exception cref="OperationCanceledException"></exception>
- public ValueTask SendAsync(ReadOnlyMemory<byte> buffer, WebSocketMessageType type, WebSocketMessageFlags flags) => WsHandle!.SendAsync(buffer, type, flags, CancellationToken.None);
+ public ValueTask SendAsync(ReadOnlyMemory<byte> buffer, WebSocketMessageType type, WebSocketMessageFlags flags)
+ => WsHandle!.SendAsync(buffer, type, flags, CancellationToken.None);
/// <summary>
@@ -219,8 +213,8 @@ namespace VNLib.Plugins.Essentials
#nullable enable
- internal WebSocketSession(string sessionId, WebSocketAcceptedCallback<T> callback)
- : base(sessionId, (ses) => callback((ses as WebSocketSession<T>)!))
+ internal WebSocketSession(WebSocketAcceptedCallback<T> callback)
+ : base((ses) => callback((ses as WebSocketSession<T>)!))
{
UserState = default;
}