diff options
author | vnugent <public@vaughnnugent.com> | 2024-05-12 16:55:32 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-05-12 16:55:32 -0400 |
commit | 4035c838c1508af0aa7e767a97431a692958ce1c (patch) | |
tree | 9c8f719db15364296fb9b18cbe559a001d925d73 /lib/Plugins.Essentials | |
parent | f4f0d4f74250257991c57bfae74c4852c7e1ae46 (diff) |
perf: Utils + http perf mods
Diffstat (limited to 'lib/Plugins.Essentials')
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()); |