aboutsummaryrefslogtreecommitdiff
path: root/lib/Plugins.Essentials
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-05-12 16:55:32 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2024-05-12 16:55:32 -0400
commit4035c838c1508af0aa7e767a97431a692958ce1c (patch)
tree9c8f719db15364296fb9b18cbe559a001d925d73 /lib/Plugins.Essentials
parentf4f0d4f74250257991c57bfae74c4852c7e1ae46 (diff)
perf: Utils + http perf mods
Diffstat (limited to 'lib/Plugins.Essentials')
-rw-r--r--lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs131
-rw-r--r--lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs4
-rw-r--r--lib/Plugins.Essentials/src/Extensions/HttpCookie.cs3
-rw-r--r--lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs15
-rw-r--r--lib/Plugins.Essentials/src/HttpEntity.cs54
-rw-r--r--lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs23
-rw-r--r--lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs24
7 files changed, 179 insertions, 75 deletions
diff --git a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
index a99b1ab..64a9611 100644
--- a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
+++ b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
@@ -114,6 +114,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <returns>true if the connection accepts any content typ, false otherwise</returns>
private static bool AcceptsAny(IConnectionInfo server)
{
+ // If no accept header is sent by clients, it is assumed it accepts all content types
if(server.Accept.Count == 0)
{
return true;
@@ -196,14 +197,15 @@ namespace VNLib.Plugins.Essentials.Extensions
//Alloc enough space to hold the string
Span<char> buffer = stackalloc char[64];
ForwardOnlyWriter<char> rangeBuilder = new(buffer);
+
//Build the range header in this format "bytes <begin>-<end>/<total>"
- rangeBuilder.Append("bytes ");
+ rangeBuilder.AppendSmall("bytes ");
rangeBuilder.Append(start);
rangeBuilder.Append('-');
rangeBuilder.Append(end);
rangeBuilder.Append('/');
rangeBuilder.Append(length);
- //Print to a string and set the content range header
+
entity.Server.Headers[HttpResponseHeader.ContentRange] = rangeBuilder.ToString();
}
@@ -212,7 +214,8 @@ namespace VNLib.Plugins.Essentials.Extensions
/// </summary>
/// <returns>true if the user-agent specified the cors security header</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsCors(this IConnectionInfo server) => "cors".Equals(server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
+ public static bool IsCors(this IConnectionInfo server)
+ => string.Equals("cors", server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Determines if the User-Agent specified "cross-site" in the Sec-Site header, OR
@@ -223,8 +226,8 @@ namespace VNLib.Plugins.Essentials.Extensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsCrossSite(this IConnectionInfo server)
{
- return "cross-site".Equals(server.Headers[SEC_HEADER_SITE], StringComparison.OrdinalIgnoreCase)
- || (server.Origin != null && !server.RequestUri.DnsSafeHost.Equals(server.Origin.DnsSafeHost, StringComparison.Ordinal));
+ return string.Equals("cross-site", server.Headers[SEC_HEADER_SITE], StringComparison.OrdinalIgnoreCase)
+ || (server.Origin != null && ! string.Equals(server.RequestUri.DnsSafeHost, server.Origin.DnsSafeHost, StringComparison.Ordinal));
}
/// <summary>
@@ -233,14 +236,16 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="server"></param>
/// <returns>true if sec-user header was set to "?1"</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsUserInvoked(this IConnectionInfo server) => "?1".Equals(server.Headers[SEC_HEADER_USER], StringComparison.OrdinalIgnoreCase);
+ public static bool IsUserInvoked(this IConnectionInfo server)
+ => string.Equals("?1", server.Headers[SEC_HEADER_USER], StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Was this request created from normal user navigation
/// </summary>
/// <returns>true if sec-mode set to "navigate"</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsNavigation(this IConnectionInfo server) => "navigate".Equals(server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
+ public static bool IsNavigation(this IConnectionInfo server)
+ => string.Equals("navigate", server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Determines if the client specified "no-cache" for the cache control header, signalling they do not wish to cache the entity
@@ -302,7 +307,11 @@ namespace VNLib.Plugins.Essentials.Extensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool RefererMatch(this IConnectionInfo server)
{
- return server.RequestUri.DnsSafeHost.Equals(server.Referer?.DnsSafeHost, StringComparison.OrdinalIgnoreCase);
+ return string.Equals(
+ server.RequestUri.DnsSafeHost,
+ server.Referer?.DnsSafeHost,
+ StringComparison.OrdinalIgnoreCase
+ );
}
/// <summary>
@@ -315,9 +324,25 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="sameSite"></param>
/// <param name="secure"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void ExpireCookie(this IConnectionInfo server, string name, string domain = "", string path = "/", CookieSameSite sameSite = CookieSameSite.None, bool secure = false)
+ public static void ExpireCookie(
+ this IConnectionInfo server,
+ string name,
+ string domain = "",
+ string path = "/",
+ CookieSameSite sameSite = CookieSameSite.None,
+ bool secure = false
+ )
{
- server.SetCookie(name, string.Empty, domain, path, TimeSpan.Zero, sameSite, false, secure);
+ SetCookie(
+ server: server,
+ name: name,
+ value: string.Empty,
+ domain: domain,
+ path: path,
+ expires: TimeSpan.Zero,
+ sameSite: sameSite,
+ secure: secure
+ );
}
/// <summary>
@@ -340,9 +365,20 @@ namespace VNLib.Plugins.Essentials.Extensions
string path = "/",
CookieSameSite sameSite = CookieSameSite.None,
bool httpOnly = false,
- bool secure = false)
+ bool secure = false
+ )
{
- server.SetCookie(name, value, domain, path, TimeSpan.MaxValue, sameSite, httpOnly, secure);
+ SetCookie(
+ server: server,
+ name: name,
+ value: value,
+ domain: domain,
+ path: path,
+ expires: TimeSpan.Zero,
+ sameSite: sameSite,
+ httpOnly: httpOnly,
+ secure: secure
+ );
}
/// <summary>
@@ -367,9 +403,24 @@ namespace VNLib.Plugins.Essentials.Extensions
string path = "/",
CookieSameSite sameSite = CookieSameSite.None,
bool httpOnly = false,
- bool secure = false)
+ bool secure = false
+ )
{
- server.SetCookie(name, value, domain, path, expires, sameSite, httpOnly, secure);
+
+ HttpResponseCookie cookie = new(name)
+ {
+ Value = value,
+ Domain = domain,
+ Path = path,
+ MaxAge = expires,
+ IsSession = expires == TimeSpan.MaxValue,
+ //If the connection is cross origin, then we need to modify the secure and samsite values
+ SameSite = sameSite,
+ HttpOnly = httpOnly,
+ Secure = secure | server.CrossOrigin,
+ };
+
+ server.SetCookie(in cookie);
}
@@ -380,35 +431,24 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="cookie">The cookie to set for the server</param>
/// <exception cref="ArgumentException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [Obsolete("HttpCookie type is obsolete in favor of HttpResponseCookie")]
public static void SetCookie(this IConnectionInfo server, in HttpCookie cookie)
{
- //Cookie name is required
- if(string.IsNullOrWhiteSpace(cookie.Name))
- {
- throw new ArgumentException("A nonn-null cookie name is required");
- }
-
//Set the cookie
- server.SetCookie(cookie.Name,
- cookie.Value,
- cookie.Domain,
- cookie.Path,
- cookie.ValidFor,
- cookie.SameSite,
- cookie.HttpOnly,
- cookie.Secure);
- }
-
- /// <summary>
- /// Is the current connection a "browser" ?
- /// </summary>
- /// <param name="server"></param>
- /// <returns>true if the user agent string contains "Mozilla" and does not contain "bot", false otherwise</returns>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsBrowser(this IConnectionInfo server)
- {
- //Get user-agent and determine if its a browser
- return server.UserAgent != null && !server.UserAgent.Contains("bot", StringComparison.OrdinalIgnoreCase) && server.UserAgent.Contains("Mozilla", StringComparison.OrdinalIgnoreCase);
+ HttpResponseCookie rCookie = new(cookie.Name)
+ {
+ Value = cookie.Value,
+ Domain = cookie.Domain,
+ Path = cookie.Path,
+ MaxAge = cookie.ValidFor,
+ IsSession = cookie.ValidFor == TimeSpan.MaxValue,
+ //If the connection is cross origin, then we need to modify the secure and samsite values
+ SameSite = cookie.SameSite,
+ HttpOnly = cookie.HttpOnly,
+ Secure = cookie.Secure | server.CrossOrigin,
+ };
+
+ server.SetCookie(in rCookie);
}
/// <summary>
@@ -417,12 +457,9 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="server"></param>
/// <returns>True of the connection was made from the local machine</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsLoopBack(this IConnectionInfo server)
- {
- IPAddress realIp = server.GetTrustedIp();
- return IPAddress.Any.Equals(realIp) || IPAddress.Loopback.Equals(realIp);
- }
-
+ public static bool IsLoopBack(this IConnectionInfo server)
+ => IPAddress.Loopback.Equals(GetTrustedIp(server));
+
/// <summary>
/// Did the connection set the dnt header?
/// </summary>
@@ -493,7 +530,7 @@ namespace VNLib.Plugins.Essentials.Extensions
//Standard https protocol header
string? protocol = server.Headers[X_FORWARDED_PROTO_HEADER];
//If the header is set and equals https then tls is being used
- return string.IsNullOrWhiteSpace(protocol) ? isSecure : "https".Equals(protocol, StringComparison.OrdinalIgnoreCase);
+ return string.IsNullOrWhiteSpace(protocol) ? isSecure : string.Equals("https", protocol, StringComparison.OrdinalIgnoreCase);
}
else
{
diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
index 8adf883..66155dd 100644
--- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
+++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
@@ -816,12 +816,12 @@ namespace VNLib.Plugins.Essentials.Extensions
public static string ContentTypeString(this in FileUpload upload) => HttpHelpers.GetContentTypeString(upload.ContentType);
/// <summary>
- /// Sets the <see cref="HttpControlMask.CompressionDisabed"/> flag on the current
+ /// Sets the <see cref="HttpControlMask.CompressionDisabled"/> flag on the current
/// <see cref="IHttpEvent"/> instance to disable dynamic compression on the response.
/// </summary>
/// <param name="entity"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void DisableCompression(this IHttpEvent entity) => entity.SetControlFlag(HttpControlMask.CompressionDisabed);
+ public static void DisableCompression(this IHttpEvent entity) => entity.SetControlFlag(HttpControlMask.CompressionDisabled);
/// <summary>
/// Attempts to upgrade the connection to a websocket, if the setup fails, it sets up the response to the client accordingly.
diff --git a/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs b/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs
index 6158a69..19c8e78 100644
--- a/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs
+++ b/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -33,6 +33,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// </summary>
/// <param name="Name">The cookie name</param>
/// <param name="Value">The cookie value</param>
+ [Obsolete("Obsolete in favor of HttpResponseCookie")]
public readonly record struct HttpCookie (string Name, string Value)
{
/// <summary>
diff --git a/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs b/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs
index f3b02dc..c4b7619 100644
--- a/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs
+++ b/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs
@@ -94,15 +94,16 @@ namespace VNLib.Plugins.Essentials.Extensions
//Only set cooke if already exists or force is true
if (entity.Server.RequestCookies.ContainsKey(Name) || force)
{
- //Build and set cookie
- HttpCookie cookie = new(Name, value)
+ HttpResponseCookie cookie = new(Name)
{
- Secure = Secure,
- HttpOnly = HttpOnly,
- ValidFor = ValidFor,
- SameSite = SameSite,
+ Value = value,
+ Domain = Domain,
Path = Path,
- Domain = Domain
+ MaxAge = ValidFor,
+ IsSession = ValidFor == TimeSpan.MaxValue,
+ SameSite = SameSite,
+ HttpOnly = HttpOnly,
+ Secure = Secure | entity.Server.CrossOrigin,
};
entity.Server.SetCookie(in cookie);
diff --git a/lib/Plugins.Essentials/src/HttpEntity.cs b/lib/Plugins.Essentials/src/HttpEntity.cs
index 2a24982..ff728e3 100644
--- a/lib/Plugins.Essentials/src/HttpEntity.cs
+++ b/lib/Plugins.Essentials/src/HttpEntity.cs
@@ -26,6 +26,7 @@ using System;
using System.IO;
using System.Net;
using System.Threading;
+using System.Diagnostics;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@@ -204,6 +205,27 @@ namespace VNLib.Plugins.Essentials
throw new ContentTypeUnacceptableException("The client does not accept the content type of the response");
}
+ /*
+ * If the underlying stream is actaully a memory stream,
+ * create a wrapper for it to read as a memory response.
+ * This is done to avoid a user-space copy since we can
+ * get access to access the internal buffer
+ *
+ * Stream length also should not cause an integer overflow,
+ * which also mean position is assumed not to overflow
+ * or cause an overflow during reading
+ */
+ if(stream is MemoryStream ms && length < int.MaxValue)
+ {
+ Entity.CloseResponse(
+ code,
+ type,
+ new MemStreamWrapper(ms, (int)length)
+ );
+
+ return;
+ }
+
Entity.CloseResponse(code, type, stream, length);
}
@@ -246,5 +268,37 @@ namespace VNLib.Plugins.Essentials
///<inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void IHttpEvent.DangerousChangeProtocol(IAlternateProtocol protocolHandler) => Entity.DangerousChangeProtocol(protocolHandler);
+
+
+ private sealed class MemStreamWrapper(MemoryStream memStream, int length) : IMemoryResponseReader
+ {
+ readonly int length = length;
+
+ /*
+ * Stream may be offset by the caller, it needs
+ * to be respected during streaming.
+ */
+ int read = (int)memStream.Position;
+
+ public int Remaining
+ {
+ get
+ {
+ Debug.Assert(length - read >= 0);
+ return length - read;
+ }
+ }
+
+ public void Advance(int written) => read += written;
+
+ ///<inheritdoc/>
+ public void Close() => memStream.Dispose();
+
+ public ReadOnlyMemory<byte> GetMemory()
+ {
+ byte[] intBuffer = memStream.GetBuffer();
+ return new ReadOnlyMemory<byte>(intBuffer, read, Remaining);
+ }
+ }
}
}
diff --git a/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs b/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs
index 2ce62b3..c3a85c9 100644
--- a/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs
+++ b/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs
@@ -33,12 +33,20 @@ namespace VNLib.Plugins.Essentials.Middleware
public async ValueTask<bool> ProcessAsync(HttpEntity entity)
{
+ /*
+ * Loops through the current linkedlist of the current middleware chain. The
+ * chain should remain unmodified after GetCurrentHead() is called.
+ *
+ * Middleware will return a Continue routine to move to the next middleware
+ * node. All other routines mean the processor has responded to the client
+ * itself and must exit control and move to response.
+ */
+
LinkedListNode<IHttpMiddleware>? mwNode = _chain.GetCurrentHead();
//Loop through nodes
while (mwNode != null)
{
- //Invoke mw handler on our event
entity.EventArgs = await mwNode.ValueRef.ProcessAsync(entity);
switch (entity.EventArgs.Routine)
@@ -60,9 +68,14 @@ namespace VNLib.Plugins.Essentials.Middleware
public void PostProcess(HttpEntity entity)
{
- LinkedListNode<IHttpMiddleware>? mwNode = _chain.GetCurrentHead();
+ /*
+ * Middleware nodes may be allowed to inspect, or modify the return
+ * event arguments as the server may not have responded to the client
+ * yet.
+ */
- //Loop through nodes
+ LinkedListNode<IHttpMiddleware>? mwNode = _chain.GetCurrentHead();
+
while (mwNode != null)
{
//Invoke mw handler on our event
@@ -72,6 +85,4 @@ namespace VNLib.Plugins.Essentials.Middleware
}
}
}
-
-
-} \ No newline at end of file
+}
diff --git a/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs b/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs
index e65c26d..b60c7c3 100644
--- a/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs
+++ b/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs
@@ -90,40 +90,40 @@ namespace VNLib.Plugins.Essentials.Oauth
ForwardOnlyWriter<char> writer = new(buffer.Span);
//Build the error message string
- writer.Append("{\"error\":\"");
+ writer.AppendSmall("{\"error\":\"");
switch (error)
{
case ErrorType.InvalidRequest:
- writer.Append("invalid_request");
+ writer.AppendSmall("invalid_request");
break;
case ErrorType.InvalidClient:
- writer.Append("invalid_client");
+ writer.AppendSmall("invalid_client");
break;
case ErrorType.UnauthorizedClient:
- writer.Append("unauthorized_client");
+ writer.AppendSmall("unauthorized_client");
break;
case ErrorType.InvalidToken:
- writer.Append("invalid_token");
+ writer.AppendSmall("invalid_token");
break;
case ErrorType.UnsupportedResponseType:
- writer.Append("unsupported_response_type");
+ writer.AppendSmall("unsupported_response_type");
break;
case ErrorType.InvalidScope:
- writer.Append("invalid_scope");
+ writer.AppendSmall("invalid_scope");
break;
case ErrorType.ServerError:
- writer.Append("server_error");
+ writer.AppendSmall("server_error");
break;
case ErrorType.TemporarilyUnavailable:
- writer.Append("temporarily_unavailable");
+ writer.AppendSmall("temporarily_unavailable");
break;
default:
- writer.Append("error");
+ writer.AppendSmall("error");
break;
}
- writer.Append("\",\"error_description\":\"");
+ writer.AppendSmall("\",\"error_description\":\"");
writer.Append(description);
- writer.Append("\"}");
+ writer.AppendSmall("\"}");
//Close the response with the json data
ev.CloseResponse(code, ContentType.Json, writer.AsSpan());