From 4035c838c1508af0aa7e767a97431a692958ce1c Mon Sep 17 00:00:00 2001 From: vnugent Date: Sun, 12 May 2024 16:55:32 -0400 Subject: perf: Utils + http perf mods --- lib/Net.Http/src/Core/ConnectionInfo.cs | 26 +-- lib/Net.Http/src/Core/HttpCookie.cs | 121 ----------- lib/Net.Http/src/Core/HttpServerProcessing.cs | 18 +- lib/Net.Http/src/Core/IHttpContextInformation.cs | 3 +- .../src/Core/PerfCounter/HttpPerfCounter.cs | 76 +++++++ .../src/Core/PerfCounter/HttpPerfCounterState.cs | 32 +++ lib/Net.Http/src/Core/Request/HttpRequest.cs | 7 +- .../Core/Response/HttpContextResponseWriting.cs | 2 +- lib/Net.Http/src/Core/Response/HttpResponse.cs | 30 +-- lib/Net.Http/src/Core/Response/ResponseWriter.cs | 27 +-- lib/Net.Http/src/Helpers/HttpControlMask.cs | 4 +- lib/Net.Http/src/Helpers/HttpHelpers.cs | 14 +- lib/Net.Http/src/HttpConfig.cs | 7 +- lib/Net.Http/src/HttpResponseCookie.cs | 226 +++++++++++++++++++++ lib/Net.Http/src/IConnectionInfo.cs | 11 +- 15 files changed, 407 insertions(+), 197 deletions(-) delete mode 100644 lib/Net.Http/src/Core/HttpCookie.cs create mode 100644 lib/Net.Http/src/Core/PerfCounter/HttpPerfCounter.cs create mode 100644 lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs create mode 100644 lib/Net.Http/src/HttpResponseCookie.cs (limited to 'lib/Net.Http') 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 Accept => Context.Request.Accept; /// - public ref readonly TransportSecurityInfo? GetTransportSecurityInfo() => ref Context.GetSecurityInfo(); + public ref readonly TransportSecurityInfo? GetTransportSecurityInfo() => ref Context.GetSecurityInfo(); /// - 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 - { - 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 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 buffer) - { - ForwardOnlyWriter 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 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(); + } + + /// + /// Gets the total time elapsed in microseconds + /// + /// The time in microseconds that has elapsed since the timer was started and stopped + 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(); } + //Create new array to hold uploads - FileUpload[] uploads = new FileUpload[_state.UploadCount]; - //Copy uploads to new array + FileUpload[] uploads = GC.AllocateUninitializedArray(_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 Cookies = new(DefaultCookieCapacity, StringComparer.OrdinalIgnoreCase); + private readonly Dictionary 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 /// /// Response header collection /// - public VnWebHeaderCollection Headers { get; } = []; + public readonly VnWebHeaderCollection Headers = []; /// /// The current http status code value @@ -88,7 +88,7 @@ namespace VNLib.Net.Http.Core.Response /// /// Cookie to add [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void AddCookie(in HttpCookie cookie) => Cookies[cookie.Name] = cookie; + internal void AddCookie(ref readonly HttpResponseCookie cookie) => Cookies[cookie.Name] = cookie; /// /// 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++) { //: \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 /// /// Tells the http server that dynamic response compression should be disabled /// - public const ulong CompressionDisabed = 0x01UL; + public const ulong CompressionDisabled = 0x01UL; /// /// 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 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. /// public readonly IHttpCompressorManager? CompressorManager { get; init; } = null; + + /// + /// Enables debug performance counters + /// + 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 +{ + /// + /// Represents an HTTP cookie that is set with responses. + /// + /// The cookie name + public readonly struct HttpResponseCookie(string name) : IStringSerializeable, IEquatable + { + /// + /// The default copy buffer allocated when calling the + /// family of methods. + /// + public const int DefaultCookieBufferSize = 4096; + + + /// + /// The name of the cookie to set. + /// + public readonly string Name { get; } = name; + + /// + /// The actual cookie content or value. + /// + public readonly string? Value { get; init; } + + /// + /// The domain this cookie will be sent to. + /// + public readonly string? Domain { get; init; } + + /// + /// The cookie path the client will send this cookie with. Null + /// or empty string for all paths. + /// + public readonly string? Path { get; init; } + + /// + /// Sets the duration of the cookie lifetime (in seconds), aka MaxAge + /// + public readonly TimeSpan MaxAge { get; init; } + + /// + /// Sets the cookie Samesite field. + /// + public readonly CookieSameSite SameSite { get; init; } + + /// + /// Sets the cookie Secure flag. If true only sends the cookie with requests + /// if the connection is secure. + /// + public readonly bool Secure { get; init; } + + /// + /// Sets cookie HttpOnly flag. If true denies JavaScript access to + /// + public readonly bool HttpOnly { get; init; } + + /// + /// Sets the cookie expiration to the duration of the user's session (aka no expiration) + /// + public readonly bool IsSession { get; init; } + + /// + /// Creates an HTTP 1.x spec cookie header value from the + /// cookie fields + /// + /// The internal copy buffer defaults to + /// use if you need control over the buffer size + /// + /// + /// The cookie header value as a string + /// + public readonly string Compile() + { + nint bufSize = MemoryUtil.NearestPage(DefaultCookieBufferSize); + + return Compile(bufSize.ToInt32()); + } + + /// + /// Creates an HTTP 1.x spec cookie header value from the + /// cookie fields. + /// + /// The size of the internal accumulator buffer + /// The cookie header value as a string + /// + public readonly string Compile(int bufferSize) + { + using UnsafeMemoryHandle cookieBuffer = MemoryUtil.UnsafeAlloc(bufferSize, false); + + ERRNO count = Compile(cookieBuffer.Span); + + return cookieBuffer.AsSpan(0, (int)count).ToString(); + } + + /// + /// Creates an HTTP 1.x spec cookie header value from the + /// cookie fields. + /// + /// The character buffer to write the cookie data tor + /// The cookie header value as a string + /// + public readonly ERRNO Compile(Span buffer) + { + ForwardOnlyWriter writer = new(buffer); + Compile(ref writer); + return writer.Written; + } + + /// + /// Writes the HTTP 1.x header format for the cookie + /// + /// + public readonly void Compile(ref ForwardOnlyWriter 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"); + } + } + + /// + public readonly override int GetHashCode() => string.GetHashCode(Name, StringComparison.OrdinalIgnoreCase); + + /// + public readonly override bool Equals(object? obj) => obj is HttpResponseCookie other && Equals(other); + + /// + public readonly bool Equals(HttpResponseCookie other) => string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase); + + /// + /// Creates an HTTP 1.x spec cookie header value from the + /// cookie fields + /// + /// The internal copy buffer defaults to + /// use if you need control over the buffer size + /// + /// + /// The cookie header value as a string + public override string ToString() => Compile(); + + /// + public static bool operator ==(HttpResponseCookie left, HttpResponseCookie right) => left.Equals(right); + + /// + 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. /// - /// Cookie name/id - /// Value to be stored in cookie - /// Domain for cookie to operate - /// Path to store cookie - /// Timespan representing how long the cookie should exist - /// Samesite attribute, Default = Lax - /// Specify the HttpOnly flag - /// Specify the Secure flag - void SetCookie(string name, string value, string? domain, string? path, TimeSpan Expires, CookieSameSite sameSite, bool httpOnly, bool secure); + /// A reference to the cookie to set on the current response + void SetCookie(in HttpResponseCookie cookie); } } \ No newline at end of file -- cgit