diff options
Diffstat (limited to 'lib')
29 files changed, 805 insertions, 331 deletions
diff --git a/lib/Net.Http/src/Core/ConnectionInfo.cs b/lib/Net.Http/src/Core/ConnectionInfo.cs index bcc5fe7..4a46971 100644 --- a/lib/Net.Http/src/Core/ConnectionInfo.cs +++ b/lib/Net.Http/src/Core/ConnectionInfo.cs @@ -88,33 +88,17 @@ namespace VNLib.Net.Http public IReadOnlyCollection<string> Accept => Context.Request.Accept; ///<inheritdoc/> - public ref readonly TransportSecurityInfo? GetTransportSecurityInfo() => ref Context.GetSecurityInfo(); + public ref readonly TransportSecurityInfo? GetTransportSecurityInfo() => ref Context.GetSecurityInfo(); ///<inheritdoc/> - public void SetCookie(string name, string value, string? domain, string? path, TimeSpan Expires, CookieSameSite sameSite, bool httpOnly, bool secure) + public void SetCookie(in HttpResponseCookie cookie) { //name MUST not be null - ArgumentNullException.ThrowIfNull(name); - - //Create the new cookie - HttpCookie cookie = new(name) - { - Value = value, - Domain = domain, - Path = path, - MaxAge = Expires, - //Set the session lifetime flag if the timeout is max value - IsSession = Expires == TimeSpan.MaxValue, - //If the connection is cross origin, then we need to modify the secure and samsite values - SameSite = CrossOrigin ? CookieSameSite.None : sameSite, - Secure = secure | CrossOrigin, - HttpOnly = httpOnly - }; - - //Set the cookie + ArgumentException.ThrowIfNullOrWhiteSpace(cookie.Name, nameof(cookie.Name)); + Context.Response.AddCookie(in cookie); } - + internal ConnectionInfo(HttpContext ctx) { //Update the context referrence diff --git a/lib/Net.Http/src/Core/HttpCookie.cs b/lib/Net.Http/src/Core/HttpCookie.cs deleted file mode 100644 index b805e3e..0000000 --- a/lib/Net.Http/src/Core/HttpCookie.cs +++ /dev/null @@ -1,121 +0,0 @@ -/* -* Copyright (c) 2024 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Net.Http -* File: HttpCookie.cs -* -* HttpCookie.cs is part of VNLib.Net.Http which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Net.Http 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.Net.Http 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 VNLib.Utils; -using VNLib.Utils.Memory; -using VNLib.Utils.Extensions; - -namespace VNLib.Net.Http.Core -{ - internal readonly struct HttpCookie(string name) : IStringSerializeable, IEquatable<HttpCookie> - { - public readonly string Name { get; } = name; - public readonly string? Value { get; init; } - public readonly string? Domain { get; init; } - public readonly string? Path { get; init; } - public readonly TimeSpan MaxAge { get; init; } - public readonly CookieSameSite SameSite { get; init; } - public readonly bool Secure { get; init; } - public readonly bool HttpOnly { get; init; } - public readonly bool IsSession { get; init; } - - public readonly string Compile() => throw new NotImplementedException(); - - public readonly void Compile(ref ForwardOnlyWriter<char> writer) - { - //set the name of the cookie - writer.Append(Name); - writer.Append('='); - - //set name - writer.Append(Value); - - //Only set the max age parameter if the cookie is not a session cookie - if (!IsSession) - { - writer.Append("; Max-Age="); - writer.Append((int)MaxAge.TotalSeconds); - } - - //Make sure domain is set - if (!string.IsNullOrWhiteSpace(Domain)) - { - writer.Append("; Domain="); - writer.Append(Domain); - } - - //Check and set path - if (!string.IsNullOrWhiteSpace(Path)) - { - //Set path - writer.Append("; Path="); - writer.Append(Path); - } - - writer.Append("; SameSite="); - - //Set the samesite flag based on the enum value - switch (SameSite) - { - case CookieSameSite.None: - writer.Append("None"); - break; - case CookieSameSite.Strict: - writer.Append("Strict"); - break; - case CookieSameSite.Lax: - default: - writer.Append("Lax"); - break; - } - - //Set httponly flag - if (HttpOnly) - { - writer.Append("; HttpOnly"); - } - - //Set secure flag - if (Secure) - { - writer.Append("; Secure"); - } - } - - public readonly ERRNO Compile(Span<char> buffer) - { - ForwardOnlyWriter<char> writer = new(buffer); - Compile(ref writer); - return writer.Written; - } - - public readonly override int GetHashCode() => string.GetHashCode(Name, StringComparison.OrdinalIgnoreCase); - - public readonly override bool Equals(object? obj) => obj is HttpCookie other && Equals(other); - - public readonly bool Equals(HttpCookie other) => Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase); - } -}
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/HttpServerProcessing.cs b/lib/Net.Http/src/Core/HttpServerProcessing.cs index 7770ad7..b6dbfef 100644 --- a/lib/Net.Http/src/Core/HttpServerProcessing.cs +++ b/lib/Net.Http/src/Core/HttpServerProcessing.cs @@ -34,9 +34,11 @@ using System.Runtime.CompilerServices; using VNLib.Utils.Memory; using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; using VNLib.Net.Http.Core; using VNLib.Net.Http.Core.Buffering; using VNLib.Net.Http.Core.Response; +using VNLib.Net.Http.Core.PerfCounter; namespace VNLib.Net.Http { @@ -167,14 +169,20 @@ namespace VNLib.Net.Http [MethodImpl(MethodImplOptions.AggressiveOptimization)] private async Task<bool> ProcessHttpEventAsync(HttpContext context) { + HttpPerfCounterState counter = default; + //Prepare http context to process a new message context.BeginRequest(); try { + HttpPerfCounter.StartCounter(ref counter); + //Try to parse the http request (may throw exceptions, let them propagate to the transport layer) int status = (int)ParseRequest(context); + HttpPerfCounter.StopAndLog(ref counter, in _config, "HTTP Parse"); + //Check status code for socket error, if so, return false to close the connection if (status >= 1000) { @@ -204,7 +212,7 @@ namespace VNLib.Net.Http context.Request.Compile(ref writer); //newline - writer.Append("\r\n"); + writer.AppendSmall("\r\n"); //Response context.Response.Compile(ref writer); @@ -218,11 +226,15 @@ namespace VNLib.Net.Http WriteConnectionDebugLog(this, context); } #endif + + HttpPerfCounter.StartCounter(ref counter); await context.WriteResponseAsync(); - + await context.FlushTransportAsync(); - + + HttpPerfCounter.StopAndLog(ref counter, in _config, "HTTP Response"); + /* * If an alternate protocol was specified, we need to break the keepalive loop * the handler will manage the alternate protocol diff --git a/lib/Net.Http/src/Core/IHttpContextInformation.cs b/lib/Net.Http/src/Core/IHttpContextInformation.cs index 14067f5..38e86b3 100644 --- a/lib/Net.Http/src/Core/IHttpContextInformation.cs +++ b/lib/Net.Http/src/Core/IHttpContextInformation.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -22,7 +22,6 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System; using System.IO; using System.Text; diff --git a/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounter.cs b/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounter.cs new file mode 100644 index 0000000..0dcefb1 --- /dev/null +++ b/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounter.cs @@ -0,0 +1,76 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpPerfCounter.cs +* +* HttpPerfCounter.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http 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.Net.Http 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 VNLib.Utils.Logging; + + +namespace VNLib.Net.Http.Core.PerfCounter +{ + internal static class HttpPerfCounter + { + + [Conditional("DEBUG")] + internal static void StartCounter(ref HttpPerfCounterState state) + { + state.StopValue = state.StartValue = TimeProvider.System.GetTimestamp(); + } + + [Conditional("DEBUG")] + internal static void StopCounter(ref HttpPerfCounterState state) + { + state.StopValue = TimeProvider.System.GetTimestamp(); + } + + /// <summary> + /// Gets the total time elapsed in microseconds + /// </summary> + /// <returns>The time in microseconds that has elapsed since the timer was started and stopped</returns> + internal static TimeSpan GetElapsedTime(ref readonly HttpPerfCounterState state) + => TimeProvider.System.GetElapsedTime(state.StartValue, state.StopValue); + + /* + * Enable http performance counters for tracing. + * Only available in debug builds until it can be + * configured for zero-cost + */ + + [Conditional("DEBUG")] + internal static void StopAndLog(ref HttpPerfCounterState state, ref readonly HttpConfig config, string counter) + { + if (!config.DebugPerformanceCounters) + { + return; + } + + StopCounter(ref state); + + TimeSpan duration = GetElapsedTime(in state); + + config.ServerLog.Debug("[PERF]: ({state}) - {us}us elapsed", counter, duration.TotalMicroseconds); + } + } +}
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs b/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs new file mode 100644 index 0000000..a86ac40 --- /dev/null +++ b/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs @@ -0,0 +1,32 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: ConnectionInfo.cs +* +* ConnectionInfo.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http 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.Net.Http 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/. +*/ + +namespace VNLib.Net.Http.Core.PerfCounter +{ + internal struct HttpPerfCounterState + { + internal long StartValue; + internal long StopValue; + } +}
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/Request/HttpRequest.cs b/lib/Net.Http/src/Core/Request/HttpRequest.cs index 3ebf0d4..2c9eed0 100644 --- a/lib/Net.Http/src/Core/Request/HttpRequest.cs +++ b/lib/Net.Http/src/Core/Request/HttpRequest.cs @@ -161,11 +161,12 @@ namespace VNLib.Net.Http.Core { return Array.Empty<FileUpload>(); } + //Create new array to hold uploads - FileUpload[] uploads = new FileUpload[_state.UploadCount]; - //Copy uploads to new array + FileUpload[] uploads = GC.AllocateUninitializedArray<FileUpload>(_state.UploadCount, false); + Array.Copy(_uploads, uploads, _state.UploadCount); - //Return new array + return uploads; } diff --git a/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs b/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs index dcd0553..93ce5b2 100644 --- a/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs +++ b/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs @@ -93,7 +93,7 @@ namespace VNLib.Net.Http.Core //Determine if compression should be used bool compressionDisabled = //disabled because app code disabled it - ContextFlags.IsSet(HttpControlMask.CompressionDisabed) + ContextFlags.IsSet(HttpControlMask.CompressionDisabled) //Disabled because too large or too small || length >= ParentServer.Config.CompressionLimit || length < ParentServer.Config.CompressionMinimum diff --git a/lib/Net.Http/src/Core/Response/HttpResponse.cs b/lib/Net.Http/src/Core/Response/HttpResponse.cs index 1340dac..e354998 100644 --- a/lib/Net.Http/src/Core/Response/HttpResponse.cs +++ b/lib/Net.Http/src/Core/Response/HttpResponse.cs @@ -46,7 +46,7 @@ namespace VNLib.Net.Http.Core.Response { const int DefaultCookieCapacity = 2; - private readonly Dictionary<string, HttpCookie> Cookies = new(DefaultCookieCapacity, StringComparer.OrdinalIgnoreCase); + private readonly Dictionary<string, HttpResponseCookie> Cookies = new(DefaultCookieCapacity, StringComparer.OrdinalIgnoreCase); private readonly DirectStream ReusableDirectStream = new(); private readonly ChunkedStream ReusableChunkedStream = new(manager.ChunkAccumulatorBuffer, ContextInfo); private readonly HeaderDataAccumulator Writer = new(manager.ResponseHeaderBuffer, ContextInfo); @@ -61,7 +61,7 @@ namespace VNLib.Net.Http.Core.Response /// <summary> /// Response header collection /// </summary> - public VnWebHeaderCollection Headers { get; } = []; + public readonly VnWebHeaderCollection Headers = []; /// <summary> /// The current http status code value @@ -88,7 +88,7 @@ namespace VNLib.Net.Http.Core.Response /// </summary> /// <param name="cookie">Cookie to add</param> [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void AddCookie(in HttpCookie cookie) => Cookies[cookie.Name] = cookie; + internal void AddCookie(ref readonly HttpResponseCookie cookie) => Cookies[cookie.Name] = cookie; /// <summary> /// Compiles and flushes all headers to the header accumulator ready for sending @@ -106,13 +106,13 @@ namespace VNLib.Net.Http.Core.Response if (!HeadersBegun) { //write status code first - writer.Append(HttpHelpers.GetResponseString(ContextInfo.CurrentVersion, _code)); - writer.Append(HttpHelpers.CRLF); + writer.AppendSmall(HttpHelpers.GetResponseString(ContextInfo.CurrentVersion, _code)); + writer.AppendSmall(HttpHelpers.CRLF); //Write the date to header buffer - writer.Append("Date: "); + writer.AppendSmall("Date: "); writer.Append(DateTimeOffset.UtcNow, "R"); - writer.Append(HttpHelpers.CRLF); + writer.AppendSmall(HttpHelpers.CRLF); //Set begun flag HeadersBegun = true; @@ -122,10 +122,10 @@ namespace VNLib.Net.Http.Core.Response for (int i = 0; i < Headers.Count; i++) { //<name>: <value>\r\n - writer.Append(Headers.Keys[i]); - writer.Append(": "); - writer.Append(Headers[i]); - writer.Append(HttpHelpers.CRLF); + writer.Append(Headers.Keys[i]); + writer.AppendSmall(": "); + writer.Append(Headers[i]); + writer.AppendSmall(HttpHelpers.CRLF); } //Remove writen headers @@ -134,14 +134,14 @@ namespace VNLib.Net.Http.Core.Response //Write cookies if any are set if (Cookies.Count > 0) { - foreach (HttpCookie cookie in Cookies.Values) + foreach (HttpResponseCookie cookie in Cookies.Values) { - writer.Append("Set-Cookie: "); + writer.AppendSmall("Set-Cookie: "); //Write the cookie to the header buffer cookie.Compile(ref writer); - writer.Append(HttpHelpers.CRLF); + writer.AppendSmall(HttpHelpers.CRLF); } Cookies.Clear(); @@ -431,7 +431,7 @@ namespace VNLib.Net.Http.Core.Response } //Enumerate and write - foreach (HttpCookie cookie in Cookies.Values) + foreach (HttpResponseCookie cookie in Cookies.Values) { writer.Append("Set-Cookie: "); diff --git a/lib/Net.Http/src/Core/Response/ResponseWriter.cs b/lib/Net.Http/src/Core/Response/ResponseWriter.cs index b60537d..fabdff3 100644 --- a/lib/Net.Http/src/Core/Response/ResponseWriter.cs +++ b/lib/Net.Http/src/Core/Response/ResponseWriter.cs @@ -182,30 +182,33 @@ namespace VNLib.Net.Http.Core.Response { if (blockSize > 0) { + /* + * Write data directly from memory response but fix the block size to the size + * of the compressor if it has one, to optimize compression + */ while (_userState.MemResponse.Remaining > 0) { - //Get next segment clamped to the block size _readSegment = _userState.MemResponse.GetRemainingConstrained(blockSize); - - //Commit output bytes + await dest.WriteAsync(_readSegment); - - //Advance by the written amount + _userState.MemResponse.Advance(_readSegment.Length); } } else - { - //Write response body from memory + { + /* + * Compressor block size is unkown so we can assume it does not matter + * and write full blocks as they are read. This will usually be a on-shot + * operation, since the writer handles chunk buffering + */ + while (_userState.MemResponse.Remaining > 0) { - //Get remaining segment _readSegment = _userState.MemResponse.GetMemory(); - - //Write segment to output stream + await dest.WriteAsync(_readSegment); - - //Advance by the written amount + _userState.MemResponse.Advance(_readSegment.Length); } } diff --git a/lib/Net.Http/src/Helpers/HttpControlMask.cs b/lib/Net.Http/src/Helpers/HttpControlMask.cs index a2a004d..e24d088 100644 --- a/lib/Net.Http/src/Helpers/HttpControlMask.cs +++ b/lib/Net.Http/src/Helpers/HttpControlMask.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -33,7 +33,7 @@ namespace VNLib.Net.Http /// <summary> /// Tells the http server that dynamic response compression should be disabled /// </summary> - public const ulong CompressionDisabed = 0x01UL; + public const ulong CompressionDisabled = 0x01UL; /// <summary> /// Tells the server not to set a 0 content length header when sending a response that does diff --git a/lib/Net.Http/src/Helpers/HttpHelpers.cs b/lib/Net.Http/src/Helpers/HttpHelpers.cs index 86616f8..cf8e189 100644 --- a/lib/Net.Http/src/Helpers/HttpHelpers.cs +++ b/lib/Net.Http/src/Helpers/HttpHelpers.cs @@ -227,29 +227,29 @@ namespace VNLib.Net.Http ForwardOnlyWriter<char> sb = new(buffer); if ((type & CacheType.NoCache) > 0) { - sb.Append("no-cache, "); + sb.AppendSmall("no-cache, "); } if ((type & CacheType.NoStore) > 0) { - sb.Append("no-store, "); + sb.AppendSmall("no-store, "); } if ((type & CacheType.Public) > 0) { - sb.Append("public, "); + sb.AppendSmall("public, "); } if ((type & CacheType.Private) > 0) { - sb.Append("private, "); + sb.AppendSmall("private, "); } if ((type & CacheType.Revalidate) > 0) { - sb.Append("must-revalidate, "); + sb.AppendSmall("must-revalidate, "); } if (immutable) { - sb.Append("immutable, "); + sb.AppendSmall("immutable, "); } - sb.Append("max-age="); + sb.AppendSmall("max-age="); sb.Append(maxAge); return sb.ToString(); } diff --git a/lib/Net.Http/src/HttpConfig.cs b/lib/Net.Http/src/HttpConfig.cs index 274e163..c74bdbb 100644 --- a/lib/Net.Http/src/HttpConfig.cs +++ b/lib/Net.Http/src/HttpConfig.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -124,5 +124,10 @@ namespace VNLib.Net.Http /// the server. /// </summary> public readonly IHttpCompressorManager? CompressorManager { get; init; } = null; + + /// <summary> + /// Enables debug performance counters + /// </summary> + public readonly bool DebugPerformanceCounters { get; init; } = false; } }
\ No newline at end of file diff --git a/lib/Net.Http/src/HttpResponseCookie.cs b/lib/Net.Http/src/HttpResponseCookie.cs new file mode 100644 index 0000000..8fc54c2 --- /dev/null +++ b/lib/Net.Http/src/HttpResponseCookie.cs @@ -0,0 +1,226 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpResponseCookie.cs +* +* HttpResponseCookie.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http 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.Net.Http 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 VNLib.Utils; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + +namespace VNLib.Net.Http +{ + /// <summary> + /// Represents an HTTP cookie that is set with responses. + /// </summary> + /// <param name="name">The cookie name</param> + public readonly struct HttpResponseCookie(string name) : IStringSerializeable, IEquatable<HttpResponseCookie> + { + /// <summary> + /// The default copy buffer allocated when calling the <see cref="Compile()"/> + /// family of methods. + /// </summary> + public const int DefaultCookieBufferSize = 4096; + + + /// <summary> + /// The name of the cookie to set. + /// </summary> + public readonly string Name { get; } = name; + + /// <summary> + /// The actual cookie content or value. + /// </summary> + public readonly string? Value { get; init; } + + /// <summary> + /// The domain this cookie will be sent to. + /// </summary> + public readonly string? Domain { get; init; } + + /// <summary> + /// The cookie path the client will send this cookie with. Null + /// or empty string for all paths. + /// </summary> + public readonly string? Path { get; init; } + + /// <summary> + /// Sets the duration of the cookie lifetime (in seconds), aka MaxAge + /// </summary> + public readonly TimeSpan MaxAge { get; init; } + + /// <summary> + /// Sets the cookie Samesite field. + /// </summary> + public readonly CookieSameSite SameSite { get; init; } + + /// <summary> + /// Sets the cookie Secure flag. If true only sends the cookie with requests + /// if the connection is secure. + /// </summary> + public readonly bool Secure { get; init; } + + /// <summary> + /// Sets cookie HttpOnly flag. If true denies JavaScript access to + /// </summary> + public readonly bool HttpOnly { get; init; } + + /// <summary> + /// Sets the cookie expiration to the duration of the user's session (aka no expiration) + /// </summary> + public readonly bool IsSession { get; init; } + + /// <summary> + /// Creates an HTTP 1.x spec cookie header value from the + /// cookie fields + /// <para> + /// The internal copy buffer defaults to <see cref="DefaultCookieBufferSize"/> + /// use <see cref="Compile(int)"/> if you need control over the buffer size + /// </para> + /// </summary> + /// <returns>The cookie header value as a string</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public readonly string Compile() + { + nint bufSize = MemoryUtil.NearestPage(DefaultCookieBufferSize); + + return Compile(bufSize.ToInt32()); + } + + /// <summary> + /// Creates an HTTP 1.x spec cookie header value from the + /// cookie fields. + /// </summary> + /// <param name="bufferSize">The size of the internal accumulator buffer</param> + /// <returns>The cookie header value as a string</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public readonly string Compile(int bufferSize) + { + using UnsafeMemoryHandle<char> cookieBuffer = MemoryUtil.UnsafeAlloc<char>(bufferSize, false); + + ERRNO count = Compile(cookieBuffer.Span); + + return cookieBuffer.AsSpan(0, (int)count).ToString(); + } + + /// <summary> + /// Creates an HTTP 1.x spec cookie header value from the + /// cookie fields. + /// </summary> + /// <param name="buffer">The character buffer to write the cookie data tor</param> + /// <returns>The cookie header value as a string</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public readonly ERRNO Compile(Span<char> buffer) + { + ForwardOnlyWriter<char> writer = new(buffer); + Compile(ref writer); + return writer.Written; + } + + /// <summary> + /// Writes the HTTP 1.x header format for the cookie + /// </summary> + /// <param name="writer"></param> + public readonly void Compile(ref ForwardOnlyWriter<char> writer) + { + writer.Append(Name); + writer.Append('='); + writer.Append(Value); + + /* + * If a session cookie is set, then do not include a max-age value + * browsers will default to session duration if not set + */ + if (!IsSession) + { + writer.AppendSmall("; Max-Age="); + writer.Append((int)MaxAge.TotalSeconds); + } + + if (!string.IsNullOrWhiteSpace(Domain)) + { + writer.AppendSmall("; Domain="); + writer.Append(Domain); + } + + if (!string.IsNullOrWhiteSpace(Path)) + { + //Set path + writer.AppendSmall("; Path="); + writer.Append(Path); + } + + writer.AppendSmall("; SameSite="); + + switch (SameSite) + { + case CookieSameSite.None: + writer.AppendSmall("None"); + break; + case CookieSameSite.Strict: + writer.AppendSmall("Strict"); + break; + case CookieSameSite.Lax: + default: + writer.AppendSmall("Lax"); + break; + } + + if (HttpOnly) + { + writer.AppendSmall("; HttpOnly"); + } + + if (Secure) + { + writer.AppendSmall("; Secure"); + } + } + + ///<inheritdoc/> + public readonly override int GetHashCode() => string.GetHashCode(Name, StringComparison.OrdinalIgnoreCase); + + ///<inheritdoc/> + public readonly override bool Equals(object? obj) => obj is HttpResponseCookie other && Equals(other); + + ///<inheritdoc/> + public readonly bool Equals(HttpResponseCookie other) => string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase); + + /// <summary> + /// Creates an HTTP 1.x spec cookie header value from the + /// cookie fields + /// <para> + /// The internal copy buffer defaults to <see cref="DefaultCookieBufferSize"/> + /// use <see cref="Compile(int)"/> if you need control over the buffer size + /// </para> + /// </summary> + /// <returns>The cookie header value as a string</returns> + public override string ToString() => Compile(); + + ///<inheritdoc/> + public static bool operator ==(HttpResponseCookie left, HttpResponseCookie right) => left.Equals(right); + + ///<inheritdoc/> + public static bool operator !=(HttpResponseCookie left, HttpResponseCookie right) => !(left == right); + } +}
\ No newline at end of file diff --git a/lib/Net.Http/src/IConnectionInfo.cs b/lib/Net.Http/src/IConnectionInfo.cs index 6cdb480..7598864 100644 --- a/lib/Net.Http/src/IConnectionInfo.cs +++ b/lib/Net.Http/src/IConnectionInfo.cs @@ -133,14 +133,7 @@ namespace VNLib.Net.Http /// Adds a new cookie to the response. If a cookie with the same name and value /// has been set, the old cookie is replaced with the new one. /// </summary> - /// <param name="name">Cookie name/id</param> - /// <param name="value">Value to be stored in cookie</param> - /// <param name="domain">Domain for cookie to operate</param> - /// <param name="path">Path to store cookie</param> - /// <param name="Expires">Timespan representing how long the cookie should exist</param> - /// <param name="sameSite">Samesite attribute, Default = Lax</param> - /// <param name="httpOnly">Specify the HttpOnly flag</param> - /// <param name="secure">Specify the Secure flag</param> - void SetCookie(string name, string value, string? domain, string? path, TimeSpan Expires, CookieSameSite sameSite, bool httpOnly, bool secure); + /// <param name="cookie">A reference to the cookie to set on the current response</param> + void SetCookie(in HttpResponseCookie cookie); } }
\ No newline at end of file diff --git a/lib/Net.Messaging.FBM/src/Client/FBMRequest.cs b/lib/Net.Messaging.FBM/src/Client/FBMRequest.cs index f16a490..e545d55 100644 --- a/lib/Net.Messaging.FBM/src/Client/FBMRequest.cs +++ b/lib/Net.Messaging.FBM/src/Client/FBMRequest.cs @@ -277,9 +277,9 @@ namespace VNLib.Net.Messaging.FBM.Client public void Compile(ref ForwardOnlyWriter<char> writer) { ReadOnlyMemory<byte> requestData = GetRequestData(); - writer.Append("Message ID:"); + writer.AppendSmall("Message ID:"); writer.Append(MessageId); - writer.Append(Environment.NewLine); + writer.AppendSmall(Environment.NewLine); Helpers.DefaultEncoding.GetChars(requestData.Span, ref writer); } ///<inheritdoc/> diff --git a/lib/Net.Rest.Client/src/Construction/Extensions.cs b/lib/Net.Rest.Client/src/Construction/Extensions.cs index ca0873b..93a1365 100644 --- a/lib/Net.Rest.Client/src/Construction/Extensions.cs +++ b/lib/Net.Rest.Client/src/Construction/Extensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Rest.Client @@ -23,6 +23,7 @@ */ using System; +using System.IO; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -46,8 +47,11 @@ namespace VNLib.Net.Rest.Client.Construction /// <param name="entity">The request entity model to send to the server</param> /// <param name="cancellation">A token to cancel the operation</param> /// <returns>A task that resolves the response message</returns> + /// <exception cref="ArgumentNullException"></exception> public static async Task<RestResponse> ExecuteAsync<TModel>(this IRestSiteAdapter site, TModel entity, CancellationToken cancellation = default) { + ArgumentNullException.ThrowIfNull(site); + //Get the adapter for the model IRestEndpointAdapter<TModel> adapter = site.GetAdapter<TModel>(); @@ -76,6 +80,44 @@ namespace VNLib.Net.Rest.Client.Construction } /// <summary> + /// Begins a stream download of the desired resource by sending the request model parameter. + /// An <see cref="IRestEndpointAdapter{TModel}"/> must be defined to handle requests of the given model type. + /// <para> + /// WARNING: This function will not invoke the OnResponse handler functions after the stream + /// has been returned, there is no way to inspect the response when excuting a stream download + /// </para> + /// </summary> + /// <typeparam name="TModel"></typeparam> + /// <param name="site"></param> + /// <param name="entity">The request entity model to send to the server</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>A task that resolves the response data stream</returns> + public static async Task<Stream?> DownloadStreamAsync<TModel>(this IRestSiteAdapter site, TModel entity, CancellationToken cancellation = default) + { + ArgumentNullException.ThrowIfNull(site); + + //Get the adapter for the model + IRestEndpointAdapter<TModel> adapter = site.GetAdapter<TModel>(); + + //Get new request on adapter + RestRequest request = adapter.GetRequest(entity); + + //Wait to exec operations if needed + await site.WaitAsync(cancellation); + + Stream? response; + + //Get rest client + using (ClientContract contract = site.GetClient()) + { + //Exec response + response = await contract.Resource.DownloadStreamAsync(request, cancellation); + } + + return response; + } + + /// <summary> /// Executes a request against the site by sending the request model parameter. An <see cref="IRestEndpointAdapter{TModel}"/> must be /// defined to handle requests of the given model type. /// </summary> @@ -87,6 +129,8 @@ namespace VNLib.Net.Rest.Client.Construction /// <returns>A task that resolves the response message with json resonse support</returns> public static async Task<RestResponse<TJson>> ExecuteAsync<TModel, TJson>(this IRestSiteAdapter site, TModel entity, CancellationToken cancellation = default) { + ArgumentNullException.ThrowIfNull(site); + //Get the adapter for the model IRestEndpointAdapter<TModel> adapter = site.GetAdapter<TModel>(); @@ -124,6 +168,8 @@ namespace VNLib.Net.Rest.Client.Construction /// <returns>When completed, gets the <see cref="RestResponse"/></returns> public static async Task<RestResponse> ExecuteSingleAsync<TModel>(this IRestSiteAdapter site, TModel model, CancellationToken cancellation = default) where TModel : IRestSingleEndpoint { + ArgumentNullException.ThrowIfNull(site); + //Init new request RestRequest request = new(model.Url, model.Method); model.OnRequest(request); @@ -149,6 +195,7 @@ namespace VNLib.Net.Rest.Client.Construction return response; } + /// <summary> /// Sets the request method of a new request /// </summary> @@ -358,22 +405,47 @@ namespace VNLib.Net.Rest.Client.Construction /// <typeparam name="TResult">The json response entity type</typeparam> /// <param name="response">The response task</param> /// <returns>A task that resolves the deserialized entity type</returns> - public static async Task<TResult?> AsJson<TResult>(this Task<RestResponse> response) + public static Task<TResult?> AsJson<TResult>(this Task<RestResponse> response) + => As(response, static r => JsonSerializer.Deserialize<TResult>(r.RawBytes)); + + /// <summary> + /// Converts a task that resolves a <see cref="RestResponse"/> to a task that deserializes + /// the response data as json. + /// </summary> + /// <param name="response">The response task</param> + /// <returns>A task that resolves the deserialized entity type</returns> + public static Task<byte[]?> AsBytes(this Task<RestResponse> response) => As(response, static p => p.RawBytes); + + /// <summary> + /// Converts a task that resolves a <see cref="RestResponse"/> to a task that uses your + /// transformation function to create the result + /// </summary> + /// <param name="response">The response task</param> + /// <param name="callback">Your custom callback function used to transform the data</param> + /// <returns>A task that resolves the deserialized entity type</returns> + public static async Task<T> As<T>(this Task<RestResponse> response, Func<RestResponse, Task<T>> callback) { + ArgumentNullException.ThrowIfNull(response); + ArgumentNullException.ThrowIfNull(callback); + RestResponse r = await response.ConfigureAwait(false); - return JsonSerializer.Deserialize<TResult>(r.RawBytes); + return await callback(r).ConfigureAwait(false); } /// <summary> - /// Converts a task that resolves a <see cref="RestResponse"/> to a task that deserializes - /// the response data as json. + /// Converts a task that resolves a <see cref="RestResponse"/> to a task that uses your + /// transformation function to create the result /// </summary> /// <param name="response">The response task</param> + /// <param name="callback">Your custom callback function used to transform the data</param> /// <returns>A task that resolves the deserialized entity type</returns> - public static async Task<byte[]?> AsBytes(this Task<RestResponse> response) + public static async Task<T> As<T>(this Task<RestResponse> response, Func<RestResponse, T> callback) { + ArgumentNullException.ThrowIfNull(response); + ArgumentNullException.ThrowIfNull(callback); + RestResponse r = await response.ConfigureAwait(false); - return r.RawBytes; + return callback(r); } private record class EndpointAdapterBuilder(IRestSiteEndpointStore Site) : IRestEndpointBuilder @@ -396,8 +468,8 @@ namespace VNLib.Net.Rest.Client.Construction ///<inheritdoc/> public IRestRequestBuilder<TModel> WithModifier(Action<TModel, RestRequest> requestBuilder) { - _ = requestBuilder ?? throw new ArgumentNullException(nameof(requestBuilder)); - //Add handler to handler chain + ArgumentNullException.ThrowIfNull(requestBuilder); + Adapter.RequestChain.AddLast(requestBuilder); return this; } @@ -405,8 +477,8 @@ namespace VNLib.Net.Rest.Client.Construction ///<inheritdoc/> public IRestRequestBuilder<TModel> WithUrl(Func<TModel, string> uriBuilder) { - _ = uriBuilder ?? throw new ArgumentNullException(nameof(uriBuilder)); - //Add get url handler + ArgumentNullException.ThrowIfNull(uriBuilder); + Adapter.GetUrl = uriBuilder; return this; } @@ -414,8 +486,8 @@ namespace VNLib.Net.Rest.Client.Construction ///<inheritdoc/> public IRestRequestBuilder<TModel> OnResponse(Action<TModel, RestResponse> onResponseBuilder) { - _ = onResponseBuilder ?? throw new ArgumentNullException(nameof(onResponseBuilder)); - //Add a response handler + ArgumentNullException.ThrowIfNull(onResponseBuilder); + Adapter.ResponseChain.AddLast(onResponseBuilder); return this; } diff --git a/lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs b/lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs index b6df58c..cb3486f 100644 --- a/lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs +++ b/lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs @@ -562,16 +562,10 @@ namespace VNLib.Net.Transport.Tcp void Stop(); } - private readonly struct TpTimerWrapper : INetTimer + private readonly struct TpTimerWrapper(Timer timer, int timeout) : INetTimer { - private readonly Timer _timer; - private readonly int _timeout; - - public TpTimerWrapper(Timer timer, int timeout) - { - _timer = timer; - _timeout = timeout; - } + private readonly Timer _timer = timer; + private readonly int _timeout = timeout; public readonly void Start() => _timer.Restart(_timeout); 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()); diff --git a/lib/Utils/src/Extensions/MemoryExtensions.cs b/lib/Utils/src/Extensions/MemoryExtensions.cs index 65d90a0..083c7cf 100644 --- a/lib/Utils/src/Extensions/MemoryExtensions.cs +++ b/lib/Utils/src/Extensions/MemoryExtensions.cs @@ -578,6 +578,27 @@ namespace VNLib.Utils.Extensions #region VnBufferWriter /// <summary> + /// Appends the string value by copying it to the internal buffer + /// </summary> + /// <param name="buffer"></param> + /// <param name="value">The string value to append to the buffer</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append(this ref ForwardOnlyWriter<char> buffer, string? value) + => buffer.Append(value.AsSpan()); + + /// <summary> + /// Appends the string value by copying it to the internal buffer + /// when the string is known to be very short. + /// </summary> + /// <param name="buffer"></param> + /// <param name="value">The string value to append to the buffer</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AppendSmall(this ref ForwardOnlyWriter<char> buffer, string? value) + => buffer.AppendSmall(value.AsSpan()); + + /// <summary> /// Formats and appends a value type to the writer with proper endianess /// </summary> /// <param name="buffer"></param> @@ -636,12 +657,20 @@ namespace VNLib.Utils.Extensions /// <param name="formatProvider"></param> /// <exception cref="ArgumentOutOfRangeException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Append<T>(this ref ForwardOnlyWriter<char> buffer, T value, ReadOnlySpan<char> format = default, IFormatProvider? formatProvider = default) where T : ISpanFormattable + public static void Append<T>( + this ref ForwardOnlyWriter<char> buffer, + T value, + ReadOnlySpan<char> format = default, + IFormatProvider? formatProvider = default + ) where T : ISpanFormattable { //Format value and write to buffer if (!value.TryFormat(buffer.Remaining, out int charsWritten, format, formatProvider)) { - throw new ArgumentOutOfRangeException(nameof(buffer), "The value could not be formatted and appended to the buffer, because there is not enough available space"); + throw new ArgumentOutOfRangeException( + nameof(buffer), + "The value could not be formatted and appended to the buffer, because there is not enough available space" + ); } //Update written posiion buffer.Advance(charsWritten); @@ -657,12 +686,20 @@ namespace VNLib.Utils.Extensions /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Append<T>(this ref ForwardOnlyMemoryWriter<char> buffer, T value, ReadOnlySpan<char> format = default, IFormatProvider? formatProvider = default) where T : ISpanFormattable + public static void Append<T>( + this ref ForwardOnlyMemoryWriter<char> buffer, + T value, + ReadOnlySpan<char> format = default, + IFormatProvider? formatProvider = default + ) where T : ISpanFormattable { //Format value and write to buffer if (!value.TryFormat(buffer.Remaining.Span, out int charsWritten, format, formatProvider)) { - throw new ArgumentOutOfRangeException(nameof(buffer), "The value could not be formatted and appended to the buffer, because there is not enough available space"); + throw new ArgumentOutOfRangeException( + nameof(buffer), + "The value could not be formatted and appended to the buffer, because there is not enough available space" + ); } //Update written posiion buffer.Advance(charsWritten); diff --git a/lib/Utils/src/Extensions/StringExtensions.cs b/lib/Utils/src/Extensions/StringExtensions.cs index c71d5a0..e9bbfbd 100644 --- a/lib/Utils/src/Extensions/StringExtensions.cs +++ b/lib/Utils/src/Extensions/StringExtensions.cs @@ -460,7 +460,7 @@ namespace VNLib.Utils.Extensions public static int Replace(this Span<char> buffer, ReadOnlySpan<char> search, ReadOnlySpan<char> replace) { ForwardOnlyWriter<char> writer = new (buffer); - writer.Replace(search, replace); + Replace(ref writer, search, replace); return writer.Written; } @@ -496,9 +496,9 @@ namespace VNLib.Utils.Extensions do { //Append the data before the search chars - writer2.Append(buffer[..start]); + writer2.Append<char>(buffer[..start]); //Append the replacment - writer2.Append(replace); + writer2.Append<char>(replace); //Shift buffer to the end of the buffer = buffer[(start + searchLen)..]; //search for next index beyond current index diff --git a/lib/Utils/src/Memory/ForwardOnlyWriter.cs b/lib/Utils/src/Memory/ForwardOnlyWriter.cs index d3c33a2..55e3b11 100644 --- a/lib/Utils/src/Memory/ForwardOnlyWriter.cs +++ b/lib/Utils/src/Memory/ForwardOnlyWriter.cs @@ -23,6 +23,7 @@ */ using System; +using System.Runtime.InteropServices; using System.Runtime.CompilerServices; namespace VNLib.Utils.Memory @@ -30,12 +31,22 @@ namespace VNLib.Utils.Memory /// <summary> /// Provides a stack based buffer writer /// </summary> - public ref struct ForwardOnlyWriter<T> + /// <remarks> + /// Creates a new <see cref="ForwardOnlyWriter{T}"/> assigning the specified buffer + /// at the specified offset + /// </remarks> + /// <param name="buffer">The buffer to write data to</param> + /// <param name="offset">The offset to begin the writer at</param> + [method: MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref struct ForwardOnlyWriter<T>(Span<T> buffer, int offset) { + //Cache reference to the first value + private readonly ref T _basePtr = ref MemoryMarshal.GetReference(buffer); + /// <summary> /// The buffer for writing output data to /// </summary> - public readonly Span<T> Buffer { get; } + public readonly Span<T> Buffer { get; } = buffer[offset..]; /// <summary> /// The number of characters written to the buffer @@ -57,43 +68,79 @@ namespace VNLib.Utils.Memory /// </summary> /// <param name="buffer">The buffer to write data to</param> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ForwardOnlyWriter(Span<T> buffer) - { - Buffer = buffer; - Written = 0; - } + public ForwardOnlyWriter(Span<T> buffer): this(buffer, 0) + { } /// <summary> - /// Creates a new <see cref="ForwardOnlyWriter{T}"/> assigning the specified buffer - /// at the specified offset + /// Returns a compiled string from the characters written to the buffer /// </summary> - /// <param name="buffer">The buffer to write data to</param> - /// <param name="offset">The offset to begin the writer at</param> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ForwardOnlyWriter(Span<T> buffer, int offset) + /// <returns>A string of the characters written to the buffer</returns> + public readonly override string ToString() => Buffer[..Written].ToString(); + + /// <summary> + /// Appends a sequence to the buffer + /// </summary> + /// <param name="data">The data sequence to append to the buffer</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public void Append<TClass>(ReadOnlySpan<T> data) where TClass : class, T { - Buffer = buffer[offset..]; - Written = 0; + //Make sure the current window is large enough to buffer the new string + ArgumentOutOfRangeException.ThrowIfGreaterThan(data.Length, RemainingSize, nameof(Remaining)); + + //write data to window + data.CopyTo(Remaining); + + //update char position + Written += data.Length; } /// <summary> - /// Returns a compiled string from the characters written to the buffer + /// Appends a sequence to the buffer of a value type by copying source + /// memory to internal buffer memory /// </summary> - /// <returns>A string of the characters written to the buffer</returns> - public readonly override string ToString() => Buffer[..Written].ToString(); + /// <typeparam name="TStruct"></typeparam> + /// <param name="data">The data sequence to append to the buffer</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public void Append<TStruct>(ReadOnlySpan<TStruct> data) where TStruct : struct, T + { + //Make sure the current window is large enough to buffer the new string + ArgumentOutOfRangeException.ThrowIfGreaterThan(data.Length, RemainingSize, nameof(Remaining)); + + //write data to window + MemoryUtil.Memmove( + in MemoryMarshal.GetReference(data), + 0, + ref Unsafe.As<T, TStruct>(ref _basePtr), //Reinterpret the ref to the local scope type, + (nuint)Written, + (nuint)data.Length + ); + + //update char position + Written += data.Length; + } /// <summary> - /// Appends a sequence to the buffer + /// Appends a sequence to the buffer of a value type by copying source + /// memory to internal buffer memory, when the buffer size is known to be + /// smaller than <see cref="ushort.MaxValue"/>. /// </summary> - /// <param name="data">The data to append to the buffer</param> + /// <typeparam name="TStruct"></typeparam> + /// <param name="data">The data sequence to append to the buffer</param> /// <exception cref="ArgumentOutOfRangeException"></exception> - public void Append(ReadOnlySpan<T> data) + public void AppendSmall<TStruct>(ReadOnlySpan<TStruct> data) where TStruct : struct, T { //Make sure the current window is large enough to buffer the new string ArgumentOutOfRangeException.ThrowIfGreaterThan(data.Length, RemainingSize, nameof(Remaining)); - Span<T> window = Buffer[Written..]; + //write data to window - data.CopyTo(window); + MemoryUtil.SmallMemmove( + in MemoryMarshal.GetReference(data), + 0, + ref Unsafe.As<T, TStruct>(ref _basePtr), //Reinterpret the ref to the local scope type, + (nuint)Written, + (ushort)data.Length + ); + //update char position Written += data.Length; } @@ -103,12 +150,21 @@ namespace VNLib.Utils.Memory /// </summary> /// <param name="c">The item to append to the buffer</param> /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(T c) { //Make sure the current window is large enough to buffer the new string ArgumentOutOfRangeException.ThrowIfZero(RemainingSize); - //Write data to buffer and increment the buffer position - Buffer[Written++] = c; + + /* + * Calc pointer to last written position. + * Written points to the address directly after the last written element + */ + + ref T offset = ref Unsafe.Add(ref _basePtr, Written); + offset = c; + + Written++; } /// <summary> @@ -116,6 +172,7 @@ namespace VNLib.Utils.Memory /// </summary> /// <param name="count">The number of elements to advance the writer by</param> /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Advance(int count) { ArgumentOutOfRangeException.ThrowIfGreaterThan(count, RemainingSize, nameof(Remaining)); @@ -126,6 +183,7 @@ namespace VNLib.Utils.Memory /// Resets the writer by setting the <see cref="Written"/> /// property to 0. /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() => Written = 0; } } diff --git a/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs b/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs index f196597..9decef7 100644 --- a/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs +++ b/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs @@ -126,7 +126,7 @@ namespace VNLib.Utils.Memory Debug.Assert(!Unsafe.IsNullRef(in srcByte), "Null source reference passed to MemmoveByRef"); Debug.Assert(!Unsafe.IsNullRef(in dstByte), "Null destination reference passed to MemmoveByRef"); - //Check for 64bit copy + //Check for 64bit copy (should get optimized away when sizeof(nuint == uint) aka 32bit platforms) if(byteCount > uint.MaxValue) { //We need a 64bit copy strategy @@ -135,7 +135,6 @@ namespace VNLib.Utils.Memory //Must be supported if(_avxCopy.Features != CopyFeatures.NotSupported) { - //Copy _avxCopy.Memmove(in srcByte, ref dstByte, byteCount); return; } @@ -144,7 +143,6 @@ namespace VNLib.Utils.Memory //try reflected memove incase it supports 64bit blocks if(_reflectedMemmove.Features != CopyFeatures.NotSupported) { - //Copy _reflectedMemmove.Memmove(in srcByte, ref dstByte, byteCount); return; } @@ -223,6 +221,7 @@ namespace VNLib.Utils.Memory public CopyFeatures Features => CopyFeatures.None; ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public void Memmove(ref readonly byte src, ref byte dst, nuint byteCount) { Debug.Assert(byteCount < uint.MaxValue, "Byte count must be less than uint.MaxValue and flags assumed 64bit blocks were supported"); |