diff options
author | vnugent <public@vaughnnugent.com> | 2023-05-10 21:57:31 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-05-10 21:57:31 -0400 |
commit | 5e1f4cf3f8bcc64114478e1547822a2f5295f6ae (patch) | |
tree | 7fe77901db253cc7bc5e992a2ba400072ad66fe2 /lib/Net.Http | |
parent | 067c692800970e6fc41fbd0df669a6b1d6a07c55 (diff) |
Yuge http buffering overhaul
Diffstat (limited to 'lib/Net.Http')
31 files changed, 1748 insertions, 813 deletions
diff --git a/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs b/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs new file mode 100644 index 0000000..25366ad --- /dev/null +++ b/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs @@ -0,0 +1,247 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: ContextLockedBufferManager.cs +* +* ContextLockedBufferManager.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/. +*/ + +/* + * This file implements the IHttpBufferManager interface, which provides + * all the required buffers for http processing flow. The design was to allocate + * a single large buffer for the entire context and then slice it up into + * smaller segments for each buffer use case. Some buffers are shared between + * operations to reduce total memory usage when it is known that buffer usage + * will not conflict. + */ + +using System; +using System.Buffers; + +using VNLib.Utils.Memory; + +namespace VNLib.Net.Http.Core.Buffering +{ + + internal class ContextLockedBufferManager : IHttpBufferManager + { + private readonly HttpBufferConfig Config; + private readonly int TotalBufferSize; + + private readonly HeaderAccumulatorBuffer _requestHeaderBuffer; + private readonly HeaderAccumulatorBuffer _responseHeaderBuffer; + private readonly ChunkAccBuffer _chunkAccBuffer; + + public ContextLockedBufferManager(in HttpBufferConfig config) + { + Config = config; + + //Compute total buffer size from server config + TotalBufferSize = ComputeTotalBufferSize(in config); + + /* + * Individual instances of the header accumulator buffer are required + * because the user controls the size of the binary buffer for responses + * and requests. The buffer segment is shared between the two instances. + */ + _requestHeaderBuffer = new(config.RequestHeaderBufferSize); + _responseHeaderBuffer = new(config.ResponseHeaderBufferSize); + + _chunkAccBuffer = new(); + } + + private IMemoryOwner<byte>? _handle; + private HttpBufferSegments<byte> _segments; + + #region LifeCycle + + ///<inheritdoc/> + public void AllocateBuffer(IHttpMemoryPool allocator) + { + //Alloc a single buffer for the entire context + _handle = allocator.AllocateBufferForContext(TotalBufferSize); + + try + { + Memory<byte> full = _handle.Memory; + + //Header parse buffer is a special case as it will be double the size due to the char buffer + int headerParseBufferSize = GetMaxHeaderBufferSize(in Config); + + //Discard/form data buffer + int discardAndFormDataSize = ComputeDiscardFormataBufferSize(in Config); + + //Slice and store the buffer segments + _segments = new() + { + //Shared header buffer + HeaderAccumulator = GetNextSegment(ref full, headerParseBufferSize), + + //Shared discard buffer and form data buffer + DiscardAndFormData = GetNextSegment(ref full, discardAndFormDataSize), + + //Buffers cannot be shared + ChunkedResponseAccumulator = GetNextSegment(ref full, Config.ChunkedResponseAccumulatorSize), + + ResponseBuffer = GetNextSegment(ref full, Config.ResponseBufferSize), + }; + + /* + * ************* WARNING **************** + * + * Request header and response header buffers are shared + * because they are assumed to be used in a single threaded context + * and control flow never allows them to be used at the same time. + * + * The bin buffer size is determined by the buffer config so the + * user may still configure the buffer size for restriction, so we + * just alloc the largerest of the two and use it for requests and + * responses. + * + * Control flow may change and become unsafe in the future! + */ + + _requestHeaderBuffer.SetBuffer(_segments.HeaderAccumulator); + _responseHeaderBuffer.SetBuffer(_segments.HeaderAccumulator); + + //Chunk buffer will be used at the same time as the response buffer and discard buffers + _chunkAccBuffer.SetBuffer(_segments.ChunkedResponseAccumulator); + } + catch + { + //Free buffer on error + _handle.Dispose(); + _handle = null; + throw; + } + } + + ///<inheritdoc/> + public void ZeroAll() + { + //Zero the buffer completely + MemoryUtil.InitializeBlock(_handle.Memory); + } + + ///<inheritdoc/> + public void FreeAll() + { + //Clear buffer memory structs to allow gc + _requestHeaderBuffer.FreeBuffer(); + _responseHeaderBuffer.FreeBuffer(); + _chunkAccBuffer.FreeBuffer(); + + //Clear segments + _segments = default; + + //Free buffer + if (_handle != null) + { + _handle.Dispose(); + _handle = null; + } + } + + #endregion + + ///<inheritdoc/> + public IHttpHeaderParseBuffer RequestHeaderParseBuffer => _requestHeaderBuffer; + + ///<inheritdoc/> + public IResponseHeaderAccBuffer ResponseHeaderBuffer => _responseHeaderBuffer; + + ///<inheritdoc/> + public IChunkAccumulatorBuffer ChunkAccumulatorBuffer => _chunkAccBuffer; + + + /* + * Discard buffer may be used for form-data parsing as they will never be used at + * the same time during normal operation + */ + + ///<inheritdoc/> + public Memory<byte> GetFormDataBuffer() => _segments.DiscardAndFormData; + + ///<inheritdoc/> + public Memory<byte> GetDiscardBuffer() => _segments.DiscardAndFormData; + + ///<inheritdoc/> + public Memory<byte> GetResponseDataBuffer() => _segments.ResponseBuffer; + + + + static Memory<byte> GetNextSegment(ref Memory<byte> buffer, int size) + { + //get segment from current slice + Memory<byte> segment = buffer[..size]; + + //Upshift buffer + buffer = buffer[size..]; + + return segment; + } + + /* + * Computes the correct size of the request header buffer from the config + * so it is large enough to hold the binary buffer but also the split char + * buffer + */ + + static int GetMaxHeaderBufferSize(in HttpBufferConfig config) + { + int max = Math.Max(config.RequestHeaderBufferSize, config.ResponseHeaderBufferSize); + + //Compute the max size including the char buffer + return SplitHttpBufferElement.GetfullSize(max); + } + + static int ComputeTotalBufferSize(in HttpBufferConfig config) + { + return config.ResponseBufferSize + + config.ChunkedResponseAccumulatorSize + + ComputeDiscardFormataBufferSize(in config) + + GetMaxHeaderBufferSize(in config); //Header buffers are shared + } + + static int ComputeDiscardFormataBufferSize(in HttpBufferConfig config) + { + //Get the larger of the two buffers, so it can be shared between the two + return Math.Max(config.DiscardBufferSize, config.FormDataBufferSize); + } + + + readonly record struct HttpBufferSegments<T> + { + public readonly Memory<T> HeaderAccumulator { get; init; } + public readonly Memory<T> ChunkedResponseAccumulator { get; init; } + public readonly Memory<T> DiscardAndFormData { get; init; } + public readonly Memory<T> ResponseBuffer { get; init; } + } + + + private sealed class HeaderAccumulatorBuffer: SplitHttpBufferElement, IResponseHeaderAccBuffer, IHttpHeaderParseBuffer + { + public HeaderAccumulatorBuffer(int binSize):base(binSize) + { } + } + + private sealed class ChunkAccBuffer : HttpBufferElement, IChunkAccumulatorBuffer + { } + } +} diff --git a/lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs b/lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs new file mode 100644 index 0000000..0e60cae --- /dev/null +++ b/lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs @@ -0,0 +1,81 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpBufferElement.cs +* +* HttpBufferElement.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.Buffers; +using System.Runtime.CompilerServices; + +using VNLib.Utils.Memory; + +namespace VNLib.Net.Http.Core.Buffering +{ + /* + * Abstract class for controlled access to the raw buffer block + * as we are pinning the block. The block is pinned once for the lifetime + * of the connection, so we have access to the raw memory for faster + * span access. + */ + internal abstract class HttpBufferElement : IHttpBuffer + { + + public virtual void FreeBuffer() + { + //Unpin and set defaults + Pinned.Dispose(); + Pinned = default; + Buffer = default; + Size = 0; + } + + public virtual void SetBuffer(Memory<byte> buffer) + { + //Set mem buffer + Buffer = buffer; + //Pin buffer and hold handle + Pinned = buffer.Pin(); + //Set size to length of buffer + Size = buffer.Length; + } + + ///<inheritdoc/> + public int Size { get; private set; } + + private MemoryHandle Pinned; + protected Memory<byte> Buffer; + + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual Span<byte> GetBinSpan() => MemoryUtil.GetSpan<byte>(Pinned, Size); + + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual Memory<byte> GetMemory() => Buffer; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual Span<byte> GetBinSpan(int maxSize) + { + return maxSize > Size ? throw new ArgumentOutOfRangeException(nameof(maxSize)) : MemoryUtil.GetSpan<byte>(Pinned, maxSize); + } + } +} diff --git a/lib/Net.Http/src/Core/Buffering/IChunkAccumulatorBuffer.cs b/lib/Net.Http/src/Core/Buffering/IChunkAccumulatorBuffer.cs new file mode 100644 index 0000000..12ce5f9 --- /dev/null +++ b/lib/Net.Http/src/Core/Buffering/IChunkAccumulatorBuffer.cs @@ -0,0 +1,32 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IChunkAccumulatorBuffer.cs +* +* IChunkAccumulatorBuffer.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.Buffering +{ + /// <summary> + /// Represents a binary only chunk accumulator buffer + /// </summary> + internal interface IChunkAccumulatorBuffer : IHttpBuffer + { } +} diff --git a/lib/Net.Http/src/Core/Buffering/IHttpBuffer.cs b/lib/Net.Http/src/Core/Buffering/IHttpBuffer.cs new file mode 100644 index 0000000..07c7618 --- /dev/null +++ b/lib/Net.Http/src/Core/Buffering/IHttpBuffer.cs @@ -0,0 +1,52 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IHttpBuffer.cs +* +* IHttpBuffer.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; + +namespace VNLib.Net.Http.Core.Buffering +{ + /// <summary> + /// Represents a buffer segment use for an http operation, and defines a shared set of + /// methods used for capturing safe buffer segments. + /// </summary> + internal interface IHttpBuffer + { + /// <summary> + /// Gets the internal buffer as a span of bytes as fast as possible + /// </summary> + /// <returns>The memory block as a span</returns> + Span<byte> GetBinSpan(); + + /// <summary> + /// Gets the internal buffer as a memory block as fast as possible + /// </summary> + /// <returns>The memory block</returns> + Memory<byte> GetMemory(); + + /// <summary> + /// The size of the internal buffer + /// </summary> + int Size { get; } + } +} diff --git a/lib/Net.Http/src/Core/Buffering/IHttpBufferManager.cs b/lib/Net.Http/src/Core/Buffering/IHttpBufferManager.cs new file mode 100644 index 0000000..445aa6f --- /dev/null +++ b/lib/Net.Http/src/Core/Buffering/IHttpBufferManager.cs @@ -0,0 +1,97 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IHttpBufferManager.cs +* +* IHttpBufferManager.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; + +namespace VNLib.Net.Http.Core.Buffering +{ + + /// <summary> + /// <para> + /// Represents an internal http buffer manager which manages the allocation and deallocation of internal buffers + /// for specific http operations. + /// </para> + /// <para> + /// Methods are considered on-demand and should be called only when the buffer is needed. + /// </para> + /// <para> + /// Properties are considered persistent, however properties and method return values + /// are considered on-demand. + /// </para> + /// </summary> + /// <remarks> + /// This abstraction assumes that the buffer manager is used in a single-threaded context. + /// </remarks> + internal interface IHttpBufferManager + { + /// <summary> + /// Gets the independent buffer block used to buffer response data + /// </summary> + /// <returns>The memory block used for buffering application response data</returns> + Memory<byte> GetResponseDataBuffer(); + + /// <summary> + /// Gets the independent buffer used to discard data request data + /// </summary> + /// <returns>The memory block used for discarding request data</returns> + Memory<byte> GetDiscardBuffer(); + + /// <summary> + /// Gets a buffer used for buffering form-data + /// </summary> + /// <returns>The memory block</returns> + Memory<byte> GetFormDataBuffer(); + + /// <summary> + /// Gets the request header parsing buffer element + /// </summary> + IHttpHeaderParseBuffer RequestHeaderParseBuffer { get; } + + /// <summary> + /// Gets the response header accumulator buffer element + /// </summary> + IResponseHeaderAccBuffer ResponseHeaderBuffer { get; } + + /// <summary> + /// Gets the chunk accumulator buffer element + /// </summary> + IChunkAccumulatorBuffer ChunkAccumulatorBuffer { get; } + + /// <summary> + /// Alloctes internal buffers from the given <see cref="IHttpMemoryPool"/> + /// </summary> + /// <param name="allocator">The pool to allocate memory from</param> + void AllocateBuffer(IHttpMemoryPool allocator); + + /// <summary> + /// Zeros all internal buffers + /// </summary> + void ZeroAll(); + + /// <summary> + /// Frees all internal buffers + /// </summary> + void FreeAll(); + } +} diff --git a/lib/Net.Http/src/Core/Buffering/IHttpHeaderParseBuffer.cs b/lib/Net.Http/src/Core/Buffering/IHttpHeaderParseBuffer.cs new file mode 100644 index 0000000..521fcfa --- /dev/null +++ b/lib/Net.Http/src/Core/Buffering/IHttpHeaderParseBuffer.cs @@ -0,0 +1,35 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IHttpHeaderParseBuffer.cs +* +* IHttpHeaderParseBuffer.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.Buffering +{ + /// <summary> + /// A split buffer element that is used to parse http request headers + /// </summary> + internal interface IHttpHeaderParseBuffer : ISplitHttpBuffer + { + + } +} diff --git a/lib/Net.Http/src/Core/Buffering/IResponseHeaderAccBuffer.cs b/lib/Net.Http/src/Core/Buffering/IResponseHeaderAccBuffer.cs new file mode 100644 index 0000000..71b1042 --- /dev/null +++ b/lib/Net.Http/src/Core/Buffering/IResponseHeaderAccBuffer.cs @@ -0,0 +1,33 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IResponseHeaderAccBuffer.cs +* +* IResponseHeaderAccBuffer.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.Buffering +{ + /// <summary> + /// Represents a split response header accumulator buffer + /// </summary> + internal interface IResponseHeaderAccBuffer : ISplitHttpBuffer + { } +} diff --git a/lib/Net.Http/src/Core/Buffering/ISplitHttpBuffer.cs b/lib/Net.Http/src/Core/Buffering/ISplitHttpBuffer.cs new file mode 100644 index 0000000..2e0963d --- /dev/null +++ b/lib/Net.Http/src/Core/Buffering/ISplitHttpBuffer.cs @@ -0,0 +1,46 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: ISplitHttpBuffer.cs +* +* ISplitHttpBuffer.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; + +namespace VNLib.Net.Http.Core.Buffering +{ + /// <summary> + /// Represents a buffer manager that contains segments for binary and character buffers + /// </summary> + internal interface ISplitHttpBuffer : IHttpBuffer + { + /// <summary> + /// Gets the character segment of the internal buffer as a span of chars, which may be slower than <see cref="IHttpBuffer.GetBinSpan"/> + /// but still considered a hot-path + /// </summary> + /// <returns>The character segment of the internal buffer</returns> + Span<char> GetCharSpan(); + + /// <summary> + /// The size of the internal binary buffer segment + /// </summary> + int BinSize { get; } + } +} diff --git a/lib/Net.Http/src/Core/Buffering/SplitHttpBufferElement.cs b/lib/Net.Http/src/Core/Buffering/SplitHttpBufferElement.cs new file mode 100644 index 0000000..e65ffd8 --- /dev/null +++ b/lib/Net.Http/src/Core/Buffering/SplitHttpBufferElement.cs @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: SplitHttpBufferElement.cs +* +* SplitHttpBufferElement.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.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace VNLib.Net.Http.Core.Buffering +{ + internal abstract class SplitHttpBufferElement : HttpBufferElement, ISplitHttpBuffer + { + ///<inheritdoc/> + public int BinSize { get; } + + internal SplitHttpBufferElement(int binSize) + { + BinSize = binSize; + } + + ///<inheritdoc/> + public Span<char> GetCharSpan() + { + //Get full buffer span + Span<byte> _base = base.GetBinSpan(); + + //Upshift to end of bin buffer + _base = _base[BinSize..]; + + //Return char span + return MemoryMarshal.Cast<byte, char>(_base); + } + + /* + * Override to trim the bin buffer to the actual size of the + * binary segment of the buffer + */ + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override Span<byte> GetBinSpan() => base.GetBinSpan(BinSize); + + + /// <summary> + /// Gets the size total of the buffer required for binary data and char data + /// </summary> + /// <param name="binSize">The desired size of the binary buffer</param> + /// <returns>The total size of the binary buffer required to store the binary and character buffer</returns> + public static int GetfullSize(int binSize) => binSize + (binSize * sizeof(char)); + } +} diff --git a/lib/Net.Http/src/Core/HttpContext.cs b/lib/Net.Http/src/Core/HttpContext.cs index 7e34dff..cdc3554 100644 --- a/lib/Net.Http/src/Core/HttpContext.cs +++ b/lib/Net.Http/src/Core/HttpContext.cs @@ -24,15 +24,15 @@ using System; using System.IO; -using System.Runtime.CompilerServices; +using System.Text; using VNLib.Utils; using VNLib.Utils.Memory.Caching; - +using VNLib.Net.Http.Core.Buffering; namespace VNLib.Net.Http.Core { - internal sealed partial class HttpContext : IConnectionContext, IReusable + internal sealed partial class HttpContext : IConnectionContext, IReusable, IHttpContextInformation { /// <summary> /// When set as a response flag, disables response compression for @@ -52,10 +52,6 @@ namespace VNLib.Net.Http.Core /// The http server that this context is bound to /// </summary> public readonly HttpServer ParentServer; - /// <summary> - /// The shared transport header reader buffer - /// </summary> - public readonly SharedHeaderReaderBuffer RequestBuffer; /// <summary> /// The response entity body container @@ -69,6 +65,11 @@ namespace VNLib.Net.Http.Core public readonly BitField ContextFlags; /// <summary> + /// The internal buffer manager for the context + /// </summary> + public readonly ContextLockedBufferManager Buffers; + + /// <summary> /// Gets or sets the alternate application protocol to swtich to /// </summary> /// <remarks> @@ -77,39 +78,26 @@ namespace VNLib.Net.Http.Core /// or this property must be exlicitly cleared /// </remarks> public IAlternateProtocol? AlternateProtocol { get; set; } + private readonly ResponseWriter responseWriter; private ITransportContext? _ctx; public HttpContext(HttpServer server) { - /* - * Local method for retreiving the transport stream, - * this adds protection/debug from response/request - * containers not allowed to maintain referrences - * to a transport stream after it has been released - */ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - Stream GetStream() => _ctx!.ConnectionStream; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - HttpVersion GetVersion() => Request.HttpVersion; - ParentServer = server; + //Store CRLF bytes + CrlfBytes = server.Config.HttpEncoding.GetBytes(HttpHelpers.CRLF); + + //Init buffer manager + Buffers = new(server.Config.BufferConfig); + //Create new request - Request = new HttpRequest(GetStream); + Request = new HttpRequest(this); //create a new response object - Response = new HttpResponse( - server.Config.HttpEncoding, - ParentServer.Config.ResponseHeaderBufferSize, - ParentServer.Config.ChunkedResponseAccumulatorSize, - GetStream, - GetVersion); - - //The shared request parsing buffer - RequestBuffer = new(server.Config.HeaderBufferSize); + Response = new HttpResponse(Buffers, this); //Init response writer ResponseBody = responseWriter = new ResponseWriter(); @@ -118,12 +106,31 @@ namespace VNLib.Net.Http.Core } public TransportSecurityInfo? GetSecurityInfo() => _ctx?.GetSecurityInfo(); - + + #region Context information + + ///<inheritdoc/> + public ReadOnlyMemory<byte> CrlfBytes { get; } + + ///<inheritdoc/> + Encoding IHttpContextInformation.Encoding => ParentServer.Config.HttpEncoding; + + ///<inheritdoc/> + HttpVersion IHttpContextInformation.CurrentVersion => Request.HttpVersion; + + ///<inheritdoc/> + HttpConfig IHttpContextInformation.Config => ParentServer.Config; + + ///<inheritdoc/> + Stream IHttpContextInformation.GetTransport() => _ctx!.ConnectionStream; + + #endregion #region LifeCycle Hooks ///<inheritdoc/> public void InitializeContext(ITransportContext ctx) => _ctx = ctx; + ///<inheritdoc/> public void BeginRequest() @@ -134,7 +141,6 @@ namespace VNLib.Net.Http.Core //Lifecycle on new request Request.OnNewRequest(); Response.OnNewRequest(); - RequestBuffer.OnNewRequest(); //Initialize the request Request.Initialize(_ctx!, ParentServer.Config.DefaultHttpVersion); @@ -145,15 +151,16 @@ namespace VNLib.Net.Http.Core { Request.OnComplete(); Response.OnComplete(); - RequestBuffer.OnComplete(); responseWriter.OnComplete(); } - + void IReusable.Prepare() { Request.OnPrepare(); Response.OnPrepare(); - RequestBuffer.OnPrepare(); + + //Alloc buffers + Buffers.AllocateBuffer(ParentServer.Config.MemoryPool); } bool IReusable.Release() @@ -165,11 +172,16 @@ namespace VNLib.Net.Http.Core //Release response/requqests Request.OnRelease(); Response.OnRelease(); - RequestBuffer.OnRelease(); + + //Zero before returning to pool + Buffers.ZeroAll(); + + //Free buffers + Buffers.FreeAll(); return true; } - + #endregion } }
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/HttpServerBase.cs b/lib/Net.Http/src/Core/HttpServerBase.cs index 3dac217..9283998 100644 --- a/lib/Net.Http/src/Core/HttpServerBase.cs +++ b/lib/Net.Http/src/Core/HttpServerBase.cs @@ -125,6 +125,7 @@ namespace VNLib.Net.Http { _ = conf.HttpEncoding ?? throw new ArgumentException("HttpEncoding cannot be null", nameof(conf)); _ = conf.ServerLog ?? throw new ArgumentException("ServerLog cannot be null", nameof(conf)); + _ = conf.MemoryPool ?? throw new ArgumentNullException(nameof(conf)); if (conf.ActiveConnectionRecvTimeout < -1) { @@ -132,7 +133,7 @@ namespace VNLib.Net.Http } //Chunked data accumulator must be at least 64 bytes (arbinrary value) - if (conf.ChunkedResponseAccumulatorSize < 64 || conf.ChunkedResponseAccumulatorSize == int.MaxValue) + if (conf.BufferConfig.ChunkedResponseAccumulatorSize < 64 || conf.BufferConfig.ChunkedResponseAccumulatorSize == int.MaxValue) { throw new ArgumentException("ChunkedResponseAccumulatorSize cannot be less than 64 bytes", nameof(conf)); } @@ -152,17 +153,17 @@ namespace VNLib.Net.Http throw new ArgumentException("DefaultHttpVersion cannot be NotSupported", nameof(conf)); } - if (conf.DiscardBufferSize < 64) + if (conf.BufferConfig.DiscardBufferSize < 64) { throw new ArgumentException("DiscardBufferSize cannot be less than 64 bytes", nameof(conf)); } - if (conf.FormDataBufferSize < 64) + if (conf.BufferConfig.FormDataBufferSize < 64) { throw new ArgumentException("FormDataBufferSize cannot be less than 64 bytes", nameof(conf)); } - if (conf.HeaderBufferSize < 128) + if (conf.BufferConfig.RequestHeaderBufferSize < 128) { throw new ArgumentException("HeaderBufferSize cannot be less than 128 bytes", nameof(conf)); } @@ -187,12 +188,12 @@ namespace VNLib.Net.Http throw new ArgumentException("MaxUploadSize cannot be less than 0", nameof(conf)); } - if (conf.ResponseBufferSize < 64) + if (conf.BufferConfig.ResponseBufferSize < 64) { throw new ArgumentException("ResponseBufferSize cannot be less than 64 bytes", nameof(conf)); } - if (conf.ResponseHeaderBufferSize < 128) + if (conf.BufferConfig.ResponseHeaderBufferSize < 128) { throw new ArgumentException("ResponseHeaderBufferSize cannot be less than 128 bytes", nameof(conf)); } diff --git a/lib/Net.Http/src/Core/HttpServerProcessing.cs b/lib/Net.Http/src/Core/HttpServerProcessing.cs index 886f735..d054755 100644 --- a/lib/Net.Http/src/Core/HttpServerProcessing.cs +++ b/lib/Net.Http/src/Core/HttpServerProcessing.cs @@ -5,8 +5,8 @@ * Package: VNLib.Net.Http * File: HttpServerProcessing.cs * -* HttpServerProcessing.cs is part of VNLib.Net.Http which is part of the larger -* VNLib collection of libraries and utilities. +* HttpServerProcessing.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 @@ -30,8 +30,10 @@ using System.Net.Sockets; using System.Threading.Tasks; using System.Runtime.CompilerServices; +using VNLib.Utils.Memory; using VNLib.Utils.Logging; using VNLib.Net.Http.Core; +using VNLib.Net.Http.Core.Buffering; namespace VNLib.Net.Http { @@ -159,16 +161,18 @@ namespace VNLib.Net.Http { return false; } + + //process the request + bool keepalive = await ProcessRequestAsync(context, (HttpStatusCode)status); + #if DEBUG - //Write debug request log - if (Config.RequestDebugLog != null) + //Write debug response log + if(Config.RequestDebugLog != null) { - Config.RequestDebugLog.Verbose(context.Request.ToString()); + WriteConnectionDebugLog(context); } #endif - //process the request - bool keepalive = await ProcessRequestAsync(context, (HttpStatusCode)status); - + //Close the response await context.WriteResponseAsync(StopToken!.Token); @@ -185,6 +189,25 @@ namespace VNLib.Net.Http } } + private void WriteConnectionDebugLog(HttpContext context) + { + //Alloc debug buffer + using IMemoryHandle<char> debugBuffer = MemoryUtil.SafeAlloc<char>(16 * 1024); + + ForwardOnlyWriter<char> writer = new (debugBuffer.Span); + + //Request + context.Request.Compile(ref writer); + + //newline + writer.Append("\r\n"); + + //Response + context.Response.Compile(ref writer); + + Config.RequestDebugLog!.Verbose("\r\n{dbg}", writer.ToString()); + } + /// <summary> /// Reads data synchronously from the transport and attempts to parse an HTTP message and /// built a request. @@ -205,34 +228,41 @@ namespace VNLib.Net.Http [MethodImpl(MethodImplOptions.AggressiveOptimization)] private HttpStatusCode ParseRequest(ITransportContext transport, HttpContext ctx) { + //Get the parse buffer + IHttpHeaderParseBuffer parseBuffer = ctx.Buffers.RequestHeaderParseBuffer; + //Init parser - TransportReader reader = new (transport.ConnectionStream, ctx.RequestBuffer, Config.HttpEncoding, HeaderLineTermination); + TransportReader reader = new (transport.ConnectionStream, parseBuffer, Config.HttpEncoding, HeaderLineTermination); try { - Span<char> lineBuf = ctx.RequestBuffer.CharBuffer; + //Get the char span + Span<char> lineBuf = parseBuffer.GetCharSpan(); Http11ParseExtensions.Http1ParseState parseState = new(); //Parse the request line - HttpStatusCode code = ctx.Request.Http1ParseRequestLine(ref parseState, ref reader, in lineBuf); + HttpStatusCode code = ctx.Request.Http1ParseRequestLine(ref parseState, ref reader, lineBuf); if (code > 0) { return code; } + //Parse the headers - code = ctx.Request.Http1ParseHeaders(ref parseState, ref reader, Config, in lineBuf); + code = ctx.Request.Http1ParseHeaders(ref parseState, ref reader, Config, lineBuf); if (code > 0) { return code; } + //Prepare entity body for request code = ctx.Request.Http1PrepareEntityBody(ref parseState, ref reader, Config); if (code > 0) { return code; } + //Success! return 0; } @@ -266,6 +296,7 @@ namespace VNLib.Net.Http //exit and close connection (default result will close the context) return false; } + //We only support version 1 and 1/1 if ((context.Request.HttpVersion & (HttpVersion.Http11 | HttpVersion.Http1)) == 0) { @@ -274,6 +305,7 @@ namespace VNLib.Net.Http context.Respond(HttpStatusCode.HttpVersionNotSupported); return false; } + //Check open connection count (not super accurate, or might not be atomic) if (OpenConnectionCount > Config.MaxOpenConnections) { @@ -297,6 +329,7 @@ namespace VNLib.Net.Http //Set connection closed context.Response.Headers[HttpResponseHeader.Connection] = "closed"; } + //Get the server root for the specified location if (!ServerRoots.TryGetValue(context.Request.Location.DnsSafeHost, out IWebRoot? root) && !ServerRoots.TryGetValue(WILDCARD_KEY, out root)) { @@ -304,6 +337,7 @@ namespace VNLib.Net.Http //make sure control leaves return keepalive; } + //check for redirects if (root.Redirects.TryGetValue(context.Request.Location.LocalPath, out Redirect? r)) { @@ -312,12 +346,14 @@ namespace VNLib.Net.Http //Return keepalive return keepalive; } + //Check the expect header and return an early status code if (context.Request.Expect) { //send a 100 status code await context.Response.SendEarly100ContinueAsync(); } + /* * Initialze the request body state, which may read/buffer the request * entity body. When doing so, the only exceptions that should be @@ -330,7 +366,9 @@ namespace VNLib.Net.Http * form data size so oom or overflow would be bugs, and we can let * them get thrown */ - await context.Request.InitRequestBodyAsync(Config.FormDataBufferSize, Config.HttpEncoding); + + await context.InitRequestBodyAsync(); + try { await ProcessAsync(root, context); diff --git a/lib/Net.Http/src/Core/IHttpContextInformation.cs b/lib/Net.Http/src/Core/IHttpContextInformation.cs new file mode 100644 index 0000000..2a6e10c --- /dev/null +++ b/lib/Net.Http/src/Core/IHttpContextInformation.cs @@ -0,0 +1,59 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IHttpContextInformation.cs +* +* IHttpContextInformation.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.IO; +using System.Text; + +namespace VNLib.Net.Http.Core +{ + internal interface IHttpContextInformation + { + /// <summary> + /// Local crlf characters + /// </summary> + ReadOnlyMemory<byte> CrlfBytes { get; } + + /// <summary> + /// The current connection's encoding + /// </summary> + Encoding Encoding { get; } + + /// <summary> + /// The current connection's http version + /// </summary> + HttpVersion CurrentVersion { get; } + + /// <summary> + /// The current server configuration + /// </summary> + HttpConfig Config { get; } + + /// <summary> + /// Gets the transport stream for the current connection. + /// </summary> + /// <returns>The current transport stream</returns> + Stream GetTransport(); + } +}
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/Request/HttpInputStream.cs b/lib/Net.Http/src/Core/Request/HttpInputStream.cs index 3b929af..c591c6d 100644 --- a/lib/Net.Http/src/Core/Request/HttpInputStream.cs +++ b/lib/Net.Http/src/Core/Request/HttpInputStream.cs @@ -24,14 +24,13 @@ using System; using System.IO; -using System.Buffers; using System.Threading; using System.Threading.Tasks; using VNLib.Utils; -using VNLib.Utils.IO; using VNLib.Utils.Memory; using VNLib.Utils.Extensions; +using VNLib.Net.Http.Core.Buffering; namespace VNLib.Net.Http.Core { @@ -40,7 +39,7 @@ namespace VNLib.Net.Http.Core /// </summary> internal sealed class HttpInputStream : Stream { - private readonly Func<Stream> GetTransport; + private readonly IHttpContextInformation ContextInfo; private long ContentLength; private Stream? InputStream; @@ -48,7 +47,7 @@ namespace VNLib.Net.Http.Core private InitDataBuffer? _initalData; - public HttpInputStream(Func<Stream> getTransport) => GetTransport = getTransport; + public HttpInputStream(IHttpContextInformation contextInfo) => ContextInfo = contextInfo; internal void OnComplete() { @@ -79,7 +78,7 @@ namespace VNLib.Net.Http.Core _initalData = initial; //Cache transport - InputStream = GetTransport(); + InputStream = ContextInfo.GetTransport(); } public override void Close() => throw new NotSupportedException("The HTTP input stream should never be closed!"); @@ -90,9 +89,10 @@ namespace VNLib.Net.Http.Core public override long Length => ContentLength; public override long Position { get => _position; set { } } - public override void Flush(){} + public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count)); + public override int Read(Span<byte> buffer) { //Calculate the amount of data that can be read into the buffer @@ -143,6 +143,7 @@ namespace VNLib.Net.Http.Core { //Calculate the amount of data that can be read into the buffer int bytesToRead = (int)Math.Min(buffer.Length, Remaining); + if (bytesToRead == 0) { return 0; @@ -168,7 +169,7 @@ namespace VNLib.Net.Http.Core if (writer.RemainingSize > 0) { //Read from transport - ERRNO read = await InputStream!.ReadAsync(writer.Remaining, cancellationToken).ConfigureAwait(false); + ERRNO read = await InputStream!.ReadAsync(writer.Remaining, cancellationToken).ConfigureAwait(true); //Update writer position writer.Advance(read); @@ -183,9 +184,9 @@ namespace VNLib.Net.Http.Core /// <summary> /// Asynchronously discards all remaining data in the stream /// </summary> - /// <param name="maxBufferSize">The maxium size of the buffer to allocate</param> + /// <param name="bufMan">The buffer manager to request the discard buffer from</param> /// <returns>A task that represents the discard operations</returns> - public async ValueTask DiscardRemainingAsync(int maxBufferSize) + public async ValueTask DiscardRemainingAsync(IHttpBufferManager bufMan) { long remaining = Remaining; @@ -203,15 +204,14 @@ namespace VNLib.Net.Http.Core //We must actaully disacrd data from the stream else { - //Calcuate a buffer size to allocate (will never be larger than an int) - int bufferSize = (int)Math.Min(remaining, maxBufferSize); - //Alloc a discard buffer to reset the transport - using IMemoryOwner<byte> discardBuffer = CoreBufferHelpers.GetMemory(bufferSize, false); - int read = 0; + //Reqest discard buffer + Memory<byte> discardBuffer = bufMan.GetDiscardBuffer(); + + int read; do { - //Read data to the discard buffer until reading is completed - read = await ReadAsync(discardBuffer.Memory, CancellationToken.None).ConfigureAwait(true); + //Read data to the discard buffer until reading is completed (read == 0) + read = await ReadAsync(discardBuffer, CancellationToken.None).ConfigureAwait(true); } while (read != 0); } @@ -222,7 +222,9 @@ namespace VNLib.Net.Http.Core //Ignore seek control return _position; } + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); } }
\ 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 356c3f6..f638ac9 100644 --- a/lib/Net.Http/src/Core/Request/HttpRequest.cs +++ b/lib/Net.Http/src/Core/Request/HttpRequest.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -23,7 +23,6 @@ */ using System; -using System.IO; using System.Net; using System.Collections.Generic; using System.Security.Authentication; @@ -35,7 +34,7 @@ using VNLib.Utils.Extensions; namespace VNLib.Net.Http.Core { - internal class HttpRequest : IHttpLifeCycle + internal sealed class HttpRequest : IHttpLifeCycle #if DEBUG ,IStringSerializeable #endif @@ -75,7 +74,7 @@ namespace VNLib.Net.Http.Core public bool Expect { get; set; } #nullable disable - public HttpRequest(Func<Stream> getTransport) + public HttpRequest(IHttpContextInformation contextInfo) { //Create new collection for headers Headers = new(); @@ -85,7 +84,7 @@ namespace VNLib.Net.Http.Core Accept = new(); AcceptLanguage = new(); //New reusable input stream - InputStream = new(getTransport); + InputStream = new(contextInfo); RequestBody = new(); } @@ -134,10 +133,11 @@ namespace VNLib.Net.Http.Core #if DEBUG + public string Compile() { //Alloc char buffer for compilation - using UnsafeMemoryHandle<char> buffer = MemoryUtil.UnsafeAlloc<char>(16 * 1024, true); + using IMemoryHandle<char> buffer = MemoryUtil.SafeAlloc<char>(16 * 1024); ForwardOnlyWriter<char> writer = new(buffer.Span); @@ -171,7 +171,9 @@ namespace VNLib.Net.Http.Core writer.Append("0.9"); break; } + writer.Append("\r\n"); + //write host writer.Append("Host: "); writer.Append(Location?.Authority); @@ -185,6 +187,7 @@ namespace VNLib.Net.Http.Core writer.Append(Headers[header]); writer.Append("\r\n"); } + //Write cookies foreach (string cookie in Cookies.Keys) { @@ -273,15 +276,12 @@ namespace VNLib.Net.Http.Core Compile(ref writer); return writer.Written; } - public override string ToString() - { - return Compile(); - } + + public override string ToString() => Compile(); #else - public override string ToString() - { - return "Request debug output only available when compiled in DEBUG mode"; - } + + public override string ToString() => "HTTP Library was compiled without a DEBUG directive, request logging is not available"; + #endif } }
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs b/lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs index 3841034..1f52d17 100644 --- a/lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs +++ b/lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs @@ -29,11 +29,10 @@ using System.Text; using System.Threading.Tasks; using System.Runtime.CompilerServices; +using VNLib.Utils.IO; using VNLib.Utils.Memory; using VNLib.Utils.Extensions; -using static VNLib.Net.Http.Core.CoreBufferHelpers; - namespace VNLib.Net.Http.Core { internal static class HttpRequestExtensions @@ -92,6 +91,7 @@ namespace VNLib.Net.Http.Core && (!Request.Origin.Authority.Equals(Request.Location.Authority, StringComparison.Ordinal) || !Request.Origin.Scheme.Equals(Request.Location.Scheme, StringComparison.Ordinal)); } + /// <summary> /// Is the current connection a websocket upgrade request handshake /// </summary> @@ -99,11 +99,13 @@ namespace VNLib.Net.Http.Core public static bool IsWebSocketRequest(this HttpRequest Request) { string? upgrade = Request.Headers[HttpRequestHeader.Upgrade]; + if (!string.IsNullOrWhiteSpace(upgrade) && upgrade.Contains("websocket", StringComparison.OrdinalIgnoreCase)) { //This request is a websocket request //Check connection header string? connection = Request.Headers[HttpRequestHeader.Connection]; + //Must be a web socket request return !string.IsNullOrWhiteSpace(connection) && connection.Contains("upgrade", StringComparison.OrdinalIgnoreCase); } @@ -130,175 +132,295 @@ namespace VNLib.Net.Http.Core /// <summary> /// Initializes the <see cref="HttpRequest.RequestBody"/> for the current request /// </summary> - /// <param name="Request"></param> - /// <param name="maxBufferSize">The maxium buffer size allowed while parsing reqeust body data</param> - /// <param name="encoding">The request data encoding for url encoded or form data bodies</param> + /// <param name="context"></param> /// <exception cref="IOException"></exception> /// <exception cref="OverflowException"></exception> /// <exception cref="OutOfMemoryException"></exception> - internal static ValueTask InitRequestBodyAsync(this HttpRequest Request, int maxBufferSize, Encoding encoding) + internal static ValueTask InitRequestBodyAsync(this HttpContext context) { - /* - * Parses query parameters from the request location query - */ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static void ParseQueryArgs(HttpRequest Request) + //Parse query + ParseQueryArgs(context.Request); + + //Decode requests from body + return !context.Request.HasEntityBody ? ValueTask.CompletedTask : ParseInputStream(context); + } + + private static async ValueTask ParseInputStream(HttpContext context) + { + HttpRequest request = context.Request; + IHttpContextInformation info = context; + IHttpMemoryPool pool = context.ParentServer.Config.MemoryPool; + + //Gets the max form data buffer size to help calculate the initial char buffer size + int maxBufferSize = context.ParentServer.Config.BufferConfig.FormDataBufferSize; + + switch (request.ContentType) + { + //CT not supported, dont read it + case ContentType.NonSupported: + break; + case ContentType.UrlEncoded: + { + //Calculate a largest available buffer to read the entire stream or up to the maximum buffer size + int bufferSize = (int)Math.Min(request.InputStream.Length, maxBufferSize); + + //Alloc the form data character buffer, this will need to grow if the form data is larger than the buffer + using MemoryHandle<char> urlbody = pool.AllocFormDataBuffer<char>(bufferSize); + + //Get a buffer for the form data + Memory<byte> formBuffer = context.Buffers.GetFormDataBuffer(); + + //Load char buffer from stream + int chars = await BufferInputStream(request.InputStream, urlbody, formBuffer, info.Encoding); + + //Get the body as a span, and split the 'string' at the & character + ((ReadOnlySpan<char>)urlbody.AsSpan(0, chars)) + .Split('&', StringSplitOptions.RemoveEmptyEntries, UrlEncodedSplitCb, request); + + } + break; + case ContentType.MultiPart: + { + //Make sure we have a boundry specified + if (string.IsNullOrWhiteSpace(request.Boundry)) + { + break; + } + + //Calculate a largest available buffer to read the entire stream or up to the maximum buffer size + int bufferSize = (int)Math.Min(request.InputStream.Length, maxBufferSize); + + //Alloc the form data buffer + using MemoryHandle<char> formBody = pool.AllocFormDataBuffer<char>(bufferSize); + + //Get a buffer for the form data + Memory<byte> formBuffer = context.Buffers.GetFormDataBuffer(); + + //Load char buffer from stream + int chars = await BufferInputStream(request.InputStream, formBody, formBuffer, info.Encoding); + + //Split the body as a span at the boundries + ((ReadOnlySpan<char>)formBody.AsSpan(0, chars)) + .Split($"--{request.Boundry}", StringSplitOptions.RemoveEmptyEntries, FormDataBodySplitCb, context); + + } + break; + //Default case is store as a file + default: + //add upload + request.RequestBody.Uploads.Add(new(request.InputStream, false, request.ContentType, null)); + break; + } + } + + /* + * Reads the input stream into the char buffer and returns the number of characters read. This method + * expands the char buffer as needed to accomodate the input stream. + * + * We assume the parsing method checked the size of the input stream so we can assume its safe to read + * all of it into memory. + */ + private static async ValueTask<int> BufferInputStream(Stream stream, MemoryHandle<char> charBuffer, Memory<byte> binBuffer, Encoding encoding) + { + int length = 0; + do { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - //Query string parse method - static void QueryParser(ReadOnlySpan<char> queryArgument, HttpRequest Request) + //read async + int read = await stream.ReadAsync(binBuffer); + + //guard + if (read <= 0) { - //Split spans after the '=' character - ReadOnlySpan<char> key = queryArgument.SliceBeforeParam('='); - ReadOnlySpan<char> value = queryArgument.SliceAfterParam('='); - //Insert into dict - Request.RequestBody.QueryArgs[key.ToString()] = value.ToString(); + break; } - //if the request has query args, parse and store them - ReadOnlySpan<char> queryString = Request.Location.Query; - if (!queryString.IsEmpty) + //calculate the number of characters + int numChars = encoding.GetCharCount(binBuffer.Span[..read]); + + //Guard for overflow + if (((ulong)(numChars + length)) >= int.MaxValue) { - //trim leading '?' if set - queryString = queryString.TrimStart('?'); - //Split args by '&' - queryString.Split('&', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries, QueryParser, Request); + throw new OverflowException(); } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static async ValueTask ParseInputStream(HttpRequest Request, int maxBufferSize, Encoding encoding) + //Re-alloc buffer + charBuffer.ResizeIfSmaller(length + numChars); + + //Decode and update position + _ = encoding.GetChars(binBuffer.Span[..read], charBuffer.Span.Slice(length, numChars)); + + //Update char count + length += numChars; + + } while (true); + + //Return the number of characters read + return length; + } + + /* + * Parses a Form-Data content type request entity body and stores those arguments in + * Request uploads or request args + */ + private static void FormDataBodySplitCb(ReadOnlySpan<char> formSegment, HttpContext state) + { + //Form data arguments + string? DispType = null, Name = null, FileName = null; + + ContentType ctHeaderVal = ContentType.NonSupported; + + //Get sliding window for parsing data + ForwardOnlyReader<char> reader = new(formSegment.TrimCRLF()); + + //Read content headers + do { - /* - * Reads all available data from the request input stream - * If the stream size is smaller than a TCP buffer size, will complete synchronously - */ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static ValueTask<VnString> ReadInputStreamAsync(HttpRequest Request, int maxBufferSize, Encoding encoding) + //Get the index of the next crlf + int index = reader.Window.IndexOf(HttpHelpers.CRLF); + + //end of headers + if (index < 1) { - //Calculate a largest available buffer to read the entire stream or up to the maximum buffer size - int bufferSize = (int)Math.Min(Request.InputStream.Length, maxBufferSize); - //Read the stream into a vnstring - return VnString.FromStreamAsync(Request.InputStream, encoding, HttpPrivateHeap, bufferSize); + break; } - /* - * SpanSplit callback function for storing UrlEncoded request entity - * bodies in key-value pairs and writing them to the - */ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static void UrlEncodedSplitCb(ReadOnlySpan<char> kvArg, HttpRequest Request) + + //Get header data + ReadOnlySpan<char> header = reader.Window[..index]; + + //Split header at colon + int colon = header.IndexOf(':'); + + //If no data is available after the colon the header is not valid, so move on to the next body + if (colon < 1) { - //Get key side of agument (or entire argument if no value is set) - ReadOnlySpan<char> key = kvArg.SliceBeforeParam('='); - ReadOnlySpan<char> value = kvArg.SliceAfterParam('='); - //trim, allocate strings, and store in the request arg dict - Request.RequestBody.RequestArgs[key.TrimCRLF().ToString()] = value.TrimCRLF().ToString(); + return; } - /* - * Parses a Form-Data content type request entity body and stores those arguments in - * Request uploads or request args - */ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static void FormDataBodySplitCb(ReadOnlySpan<char> formSegment, ValueTuple<HttpRequestBody, Encoding> state) + //Hash the header value into a header enum + HttpRequestHeader headerType = HttpHelpers.GetRequestHeaderEnumFromValue(header[..colon]); + + //get the header value + ReadOnlySpan<char> headerValue = header[(colon + 1)..]; + + //Check for content dispositon header + if (headerType == HttpHelpers.ContentDisposition) { - //Form data arguments - string? DispType = null, Name = null, FileName = null; - ContentType ctHeaderVal = ContentType.NonSupported; - //Get sliding window for parsing data - ForwardOnlyReader<char> reader = new(formSegment.TrimCRLF()); - //Read content headers - do - { - //Get the index of the next crlf - int index = reader.Window.IndexOf(HttpHelpers.CRLF); - //end of headers - if (index < 1) - { - break; - } - //Get header data - ReadOnlySpan<char> header = reader.Window[..index]; - //Split header at colon - int colon = header.IndexOf(':'); - //If no data is available after the colon the header is not valid, so move on to the next body - if (colon < 1) - { - return; - } - //Hash the header value into a header enum - HttpRequestHeader headerType = HttpHelpers.GetRequestHeaderEnumFromValue(header[..colon]); - //get the header value - ReadOnlySpan<char> headerValue = header[(colon + 1)..]; - //Check for content dispositon header - if (headerType == HttpHelpers.ContentDisposition) - { - //Parse the content dispostion - HttpHelpers.ParseDisposition(headerValue, out DispType, out Name, out FileName); - } - //Check for content type - else if (headerType == HttpRequestHeader.ContentType) - { - //The header value for content type should be an MIME content type - ctHeaderVal = HttpHelpers.GetContentType(headerValue.Trim().ToString()); - } - //Shift window to the next line - reader.Advance(index + HttpHelpers.CRLF.Length); - } while (true); - //Remaining data should be the body data (will have leading and trailing CRLF characters - //If filename is set, this must be a file - if (!string.IsNullOrWhiteSpace(FileName)) - { - //Store the file in the uploads - state.Item1.Uploads.Add(FileUpload.FromString(reader.Window.TrimCRLF(), state.Item2, FileName, ctHeaderVal)); - } - //Make sure the name parameter was set and store the message body as a string - else if (!string.IsNullOrWhiteSpace(Name)) - { - //String data as body - state.Item1.RequestArgs[Name] = reader.Window.TrimCRLF().ToString(); - } + //Parse the content dispostion + HttpHelpers.ParseDisposition(headerValue, out DispType, out Name, out FileName); } - - switch (Request.ContentType) + //Check for content type + else if (headerType == HttpRequestHeader.ContentType) { - //CT not supported, dont read it - case ContentType.NonSupported: - break; - case ContentType.UrlEncoded: - //Create a vnstring from the message body and parse it (assuming url encoded bodies are small so a small stack buffer will be fine) - using (VnString urlbody = await ReadInputStreamAsync(Request, maxBufferSize, encoding)) - { - //Get the body as a span, and split the 'string' at the & character - urlbody.AsSpan().Split('&', StringSplitOptions.RemoveEmptyEntries, UrlEncodedSplitCb, Request); - } - break; - case ContentType.MultiPart: - //Make sure we have a boundry specified - if (string.IsNullOrWhiteSpace(Request.Boundry)) - { - break; - } - //Read all data from stream into string - using (VnString body = await ReadInputStreamAsync(Request, maxBufferSize, encoding)) - { - //Split the body as a span at the boundries - body.AsSpan().Split($"--{Request.Boundry}", StringSplitOptions.RemoveEmptyEntries, FormDataBodySplitCb, (Request.RequestBody, encoding)); - } - break; - //Default case is store as a file - default: - //add upload - Request.RequestBody.Uploads.Add(new(Request.InputStream, string.Empty, Request.ContentType, false)); - break; + //The header value for content type should be an MIME content type + ctHeaderVal = HttpHelpers.GetContentType(headerValue.Trim().ToString()); } + + //Shift window to the next line + reader.Advance(index + HttpHelpers.CRLF.Length); + + } while (true); + + //Remaining data should be the body data (will have leading and trailing CRLF characters + //If filename is set, this must be a file + if (!string.IsNullOrWhiteSpace(FileName)) + { + ReadOnlySpan<char> fileData = reader.Window.TrimCRLF(); + + FileUpload upload = UploadFromString(fileData, state, FileName, ctHeaderVal); + + //Store the file in the uploads + state.Request.RequestBody.Uploads.Add(upload); } - //Parse query - ParseQueryArgs(Request); + //Make sure the name parameter was set and store the message body as a string + else if (!string.IsNullOrWhiteSpace(Name)) + { + //String data as body + state.Request.RequestBody.RequestArgs[Name] = reader.Window.TrimCRLF().ToString(); + } + } - //Decode requests from body - return !Request.HasEntityBody ? ValueTask.CompletedTask : ParseInputStream(Request, maxBufferSize, encoding); + /// <summary> + /// Allocates a new binary buffer, encodes, and copies the specified data to a new <see cref="FileUpload"/> + /// structure of the specified content type + /// </summary> + /// <param name="data">The string data to copy</param> + /// <param name="context">The connection context</param> + /// <param name="filename">The name of the file</param> + /// <param name="ct">The content type of the file data</param> + /// <returns>The <see cref="FileUpload"/> container</returns> + private static FileUpload UploadFromString(ReadOnlySpan<char> data, HttpContext context, string filename, ContentType ct) + { + IHttpContextInformation info = context; + IHttpMemoryPool pool = context.ParentServer.Config.MemoryPool; + + //get number of bytes + int bytes = info.Encoding.GetByteCount(data); + + //get a buffer from the HTTP heap + MemoryHandle<byte> buffHandle = pool.AllocFormDataBuffer<byte>(bytes); + try + { + //Convert back to binary + bytes = info.Encoding.GetBytes(data, buffHandle); + + //Create a new memory stream encapsulating the file data + VnMemoryStream vms = VnMemoryStream.ConsumeHandle(buffHandle, bytes, true); + + //Create new upload wrapper that owns the stream + return new(vms, true, ct, filename); + } + catch + { + //Make sure the hanle gets disposed if there is an error + buffHandle.Dispose(); + throw; + } + } + + /* + * SpanSplit callback function for storing UrlEncoded request entity + * bodies in key-value pairs and writing them to the + */ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UrlEncodedSplitCb(ReadOnlySpan<char> kvArg, HttpRequest Request) + { + //Get key side of agument (or entire argument if no value is set) + ReadOnlySpan<char> key = kvArg.SliceBeforeParam('='); + ReadOnlySpan<char> value = kvArg.SliceAfterParam('='); + + //trim, allocate strings, and store in the request arg dict + Request.RequestBody.RequestArgs[key.TrimCRLF().ToString()] = value.TrimCRLF().ToString(); + } + + /* + * Parses query parameters from the request location query + */ + private static void ParseQueryArgs(HttpRequest Request) + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + //Query string parse method + static void QueryParser(ReadOnlySpan<char> queryArgument, HttpRequest Request) + { + //Split spans at the '=' character + ReadOnlySpan<char> key = queryArgument.SliceBeforeParam('='); + ReadOnlySpan<char> value = queryArgument.SliceAfterParam('='); + + //Insert into dict + Request.RequestBody.QueryArgs[key.ToString()] = value.ToString(); + } + + //if the request has query args, parse and store them + ReadOnlySpan<char> queryString = Request.Location.Query; + + if (!queryString.IsEmpty) + { + //trim leading '?' if set + queryString = queryString.TrimStart('?'); + + //Split args by '&' + queryString.Split('&', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries, QueryParser, Request); + } } } }
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs b/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs index f633a4a..f7e17f7 100644 --- a/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs +++ b/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs @@ -62,7 +62,7 @@ namespace VNLib.Net.Http.Core /// <returns>0 if the request line was successfully parsed, a status code if the request could not be processed</returns> /// <exception cref="UriFormatException"></exception> [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] - public static HttpStatusCode Http1ParseRequestLine(this HttpRequest Request, ref Http1ParseState parseState, ref TransportReader reader, in Span<char> lineBuf) + public static HttpStatusCode Http1ParseRequestLine(this HttpRequest Request, ref Http1ParseState parseState, ref TransportReader reader, Span<char> lineBuf) { //Locals ERRNO requestResult; @@ -70,6 +70,7 @@ namespace VNLib.Net.Http.Core //Read the start line requestResult = reader.ReadLine(lineBuf); + //Must be able to parse the verb and location if (requestResult < 1) { @@ -79,6 +80,7 @@ namespace VNLib.Net.Http.Core //true up the request line to actual size ReadOnlySpan<char> requestLine = lineBuf[..(int)requestResult].Trim(); + //Find the first white space character ("GET / HTTP/1.1") index = requestLine.IndexOf(' '); if (index == -1) @@ -88,6 +90,7 @@ namespace VNLib.Net.Http.Core //Decode the verb (function requires the string be the exact characters of the request method) Request.Method = HttpHelpers.GetRequestMethod(requestLine[0..index]); + //Make sure the method is supported if (Request.Method == HttpMethod.None) { @@ -96,6 +99,7 @@ namespace VNLib.Net.Http.Core //location string should be from end of verb to HTTP/ NOTE: Only supports http... this is an http server endloc = requestLine.LastIndexOf(" HTTP/", StringComparison.OrdinalIgnoreCase); + //Client must specify an http version prepended by a single whitespace(rfc2612) if (endloc == -1) { @@ -134,8 +138,10 @@ namespace VNLib.Net.Http.Core //Set a default scheme Scheme = Request.EncryptionVersion == SslProtocols.None ? Uri.UriSchemeHttp : Uri.UriSchemeHttps, }; + //Need to manually parse the query string int q = paq.IndexOf('?'); + //has query? if (q == -1) { @@ -164,7 +170,7 @@ namespace VNLib.Net.Http.Core /// <param name="lineBuf">The buffer read data from the transport with</param> /// <returns>0 if the request line was successfully parsed, a status code if the request could not be processed</returns> [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] - public static HttpStatusCode Http1ParseHeaders(this HttpRequest Request, ref Http1ParseState parseState, ref TransportReader reader, in HttpConfig Config, in Span<char> lineBuf) + public static HttpStatusCode Http1ParseHeaders(this HttpRequest Request, ref Http1ParseState parseState, ref TransportReader reader, in HttpConfig Config, Span<char> lineBuf) { try { @@ -253,6 +259,7 @@ namespace VNLib.Net.Http.Core { //Update keepalive, if the connection header contains "closed" and with the current value of keepalive Request.KeepAlive &= !requestHeaderValue.Contains("close", StringComparison.OrdinalIgnoreCase); + //Also store the connecion header into the store Request.Headers.Add(HttpRequestHeader.Connection, requestHeaderValue.ToString()); } @@ -264,8 +271,10 @@ namespace VNLib.Net.Http.Core //Invalid content type header value return HttpStatusCode.UnsupportedMediaType; } + Request.Boundry = boundry; Request.Charset = charset; + //Get the content type enum from mime type Request.ContentType = HttpHelpers.GetContentType(ct); } @@ -303,6 +312,7 @@ namespace VNLib.Net.Http.Core //Split the host value by the port parameter ReadOnlySpan<char> port = requestHeaderValue.SliceAfterParam(':').Trim(); + //Slicing beofre the colon should always provide a useable hostname, so allocate a string for it string host = requestHeaderValue.SliceBeforeParam(':').Trim().ToString(); @@ -344,8 +354,8 @@ namespace VNLib.Net.Http.Core { //Get the name parameter and alloc a string string name = cookie.SliceBeforeParam('=').Trim().ToString(); - //Get the value parameter and alloc a string string value = cookie.SliceAfterParam('=').Trim().ToString(); + //Add the cookie to the dictionary _ = cookieContainer.TryAdd(name, value); } @@ -374,15 +384,19 @@ namespace VNLib.Net.Http.Core { //See if range bytes value has been set ReadOnlySpan<char> rawRange = requestHeaderValue.SliceAfterParam("bytes=").TrimCRLF(); + //Make sure the bytes parameter is set if (rawRange.IsEmpty) { break; } + //Get start range ReadOnlySpan<char> startRange = rawRange.SliceBeforeParam('-'); + //Get end range (empty if no - exists) ReadOnlySpan<char> endRange = rawRange.SliceAfterParam('-'); + //See if a range end is specified if (endRange.IsEmpty) { @@ -413,6 +427,7 @@ namespace VNLib.Net.Http.Core { //Alloc a string for origin string origin = requestHeaderValue.ToString(); + //Origin headers should always be absolute address "parsable" if (Uri.TryCreate(origin, UriKind.Absolute, out Uri? org)) { @@ -495,6 +510,7 @@ namespace VNLib.Net.Http.Core //Check for chuncked transfer encoding ReadOnlySpan<char> transfer = Request.Headers[HttpRequestHeader.TransferEncoding]; + if (!transfer.IsEmpty && transfer.Contains("chunked", StringComparison.OrdinalIgnoreCase)) { //Not a valid http version for chunked transfer encoding @@ -502,6 +518,7 @@ namespace VNLib.Net.Http.Core { return HttpStatusCode.BadRequest; } + /* * Was a content length also specified? * This is an issue and is likely an attack. I am choosing not to support diff --git a/lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs b/lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs index 35c0275..9ea06f3 100644 --- a/lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs +++ b/lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -23,15 +23,10 @@ */ using System; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; using VNLib.Utils; using VNLib.Utils.IO; - -using static VNLib.Net.Http.Core.CoreBufferHelpers; +using VNLib.Net.Http.Core.Buffering; namespace VNLib.Net.Http.Core { @@ -41,36 +36,53 @@ namespace VNLib.Net.Http.Core /// </summary> internal class ChunkDataAccumulator : IDataAccumulator<byte>, IHttpLifeCycle { + private const string LAST_CHUNK_STRING = "0\r\n\r\n"; public const int RESERVED_CHUNK_SUGGESTION = 32; - - private readonly int BufferSize; + private readonly int ReservedSize; - private readonly Encoding Encoding; - private readonly ReadOnlyMemory<byte> CRLFBytes; + private readonly IHttpContextInformation Context; + private readonly IChunkAccumulatorBuffer Buffer; + private readonly ReadOnlyMemory<byte> LastChunk; - public ChunkDataAccumulator(Encoding encoding, int bufferSize) + public ChunkDataAccumulator(IChunkAccumulatorBuffer buffer, IHttpContextInformation context) { - Encoding = encoding; - CRLFBytes = encoding.GetBytes(HttpHelpers.CRLF); - ReservedSize = RESERVED_CHUNK_SUGGESTION; - BufferSize = bufferSize; - } - private byte[]? _buffer; + Context = context; + Buffer = buffer; + + //Convert and store cached versions of the last chunk bytes + LastChunk = context.Encoding.GetBytes(LAST_CHUNK_STRING); + } + + /* + * Reserved offset is a pointer to the first byte of the reserved chunk window + * that actually contains the size segment data. + */ + private int _reservedOffset; ///<inheritdoc/> - public int RemainingSize => _buffer!.Length - AccumulatedSize; + public int RemainingSize => Buffer.Size - AccumulatedSize; + ///<inheritdoc/> - public Span<byte> Remaining => _buffer!.AsSpan(AccumulatedSize); + public Span<byte> Remaining => Buffer.GetBinSpan()[AccumulatedSize..]; + ///<inheritdoc/> - public Span<byte> Accumulated => _buffer!.AsSpan(_reservedOffset, AccumulatedSize); + public Span<byte> Accumulated => Buffer.GetBinSpan()[_reservedOffset.. AccumulatedSize]; + ///<inheritdoc/> public int AccumulatedSize { get; set; } - private Memory<byte> CompleteChunk => _buffer.AsMemory(_reservedOffset, (AccumulatedSize - _reservedOffset)); + /* + * Completed chunk is the segment of the buffer that contains the size segment + * followed by the accumulated chunk data, and the trailing crlf. + * + * AccumulatedSize points to the end of the accumulated chunk data. The reserved + * offset points to the start of the size segment. + */ + private Memory<byte> GetCompleteChunk() => Buffer.GetMemory()[_reservedOffset..AccumulatedSize]; /// <summary> /// Attempts to buffer as much data as possible from the specified data @@ -80,10 +92,11 @@ namespace VNLib.Net.Http.Core public ERRNO TryBufferChunk(ReadOnlySpan<byte> data) { //Calc data size and reserve space for final crlf - int dataToCopy = Math.Min(data.Length, RemainingSize - CRLFBytes.Length); + int dataToCopy = Math.Min(data.Length, RemainingSize - Context.CrlfBytes.Length); //Write as much data as possible data[..dataToCopy].CopyTo(Remaining); + //Advance buffer Advance(dataToCopy); @@ -96,7 +109,7 @@ namespace VNLib.Net.Http.Core private void InitReserved() { - //First reserve the chunk window by advancing the accumulator to the size + //First reserve the chunk window by advancing the accumulator to the reserved size Advance(ReservedSize); } @@ -111,48 +124,59 @@ namespace VNLib.Net.Http.Core } /// <summary> - /// Writes the buffered data as a single chunk to the stream asynchronously. The internal - /// state is reset if writing compleded successfully + /// Complets and returns the memory segment containing the chunk data to send + /// to the client. This also resets the accumulator. /// </summary> - /// <param name="output">The stream to write data to</param> - /// <param name="cancellation">A token to cancel the operation</param> - /// <returns>A value task that resolves when the data has been written to the stream</returns> - public async ValueTask FlushAsync(Stream output, CancellationToken cancellation) + /// <returns></returns> + public Memory<byte> GetChunkData() { //Update the chunk size UpdateChunkSize(); //Write trailing chunk delimiter - this.Append(CRLFBytes.Span); - - //write to stream - await output.WriteAsync(CompleteChunk, cancellation); + this.Append(Context.CrlfBytes.Span); - //Reset for next chunk - Reset(); + return GetCompleteChunk(); } /// <summary> - /// Writes the buffered data as a single chunk to the stream. The internal - /// state is reset if writing compleded successfully + /// Complets and returns the memory segment containing the chunk data to send + /// to the client. /// </summary> - /// <param name="output">The stream to write data to</param> - /// <returns>A value task that resolves when the data has been written to the stream</returns> - public void Flush(Stream output) + /// <returns></returns> + public Memory<byte> GetFinalChunkData() { //Update the chunk size UpdateChunkSize(); //Write trailing chunk delimiter - this.Append(CRLFBytes.Span); + this.Append(Context.CrlfBytes.Span); - //write to stream - output.Write(CompleteChunk.Span); + //Write final chunk to the end of the accumulator + this.Append(LastChunk.Span); - //Reset for next chunk - Reset(); + return GetCompleteChunk(); } + + /* + * UpdateChunkSize method updates the running total of the chunk size + * in the reserved segment of the buffer. This is because http chunking + * requires hex encoded chunk sizes to be written as the first bytes of + * the chunk. So when the flush methods are called, the chunk size + * at the beginning of the chunk is updated to reflect the total size. + * + * Because we need to store space at the head of the chunk for the size + * we need to reserve space for the size segment. + * + * The size sigment bytes abutt the chunk data bytes, so the size segment + * is stored at the end of the reserved segment, which is directly before + * the start of the chunk data. + * + * [reserved segment] [chunk data] [eoc] + * [...0a \r\n] [10 bytes of data] [eoc] + */ + private void UpdateChunkSize() { const int CharBufSize = 2 * sizeof(int); @@ -173,7 +197,7 @@ namespace VNLib.Net.Http.Core //temp buffer to store encoded data in Span<byte> encBuf = stackalloc byte[ReservedSize]; //Encode the chunk size chars - int initOffset = Encoding.GetBytes(s[..written], encBuf); + int initOffset = Context.Encoding.GetBytes(s[..written], encBuf); Span<byte> encoded = encBuf[..initOffset]; @@ -185,9 +209,9 @@ namespace VNLib.Net.Http.Core * the exact size required to store the encoded chunk size */ - _reservedOffset = (ReservedSize - (initOffset + CRLFBytes.Length)); + _reservedOffset = (ReservedSize - (initOffset + Context.CrlfBytes.Length)); - Span<byte> upshifted = _buffer!.AsSpan(_reservedOffset, ReservedSize); + Span<byte> upshifted = Buffer.GetBinSpan()[_reservedOffset..ReservedSize]; //First write the chunk size encoded.CopyTo(upshifted); @@ -196,7 +220,7 @@ namespace VNLib.Net.Http.Core upshifted = upshifted[initOffset..]; //Copy crlf - CRLFBytes.Span.CopyTo(upshifted); + Context.CrlfBytes.Span.CopyTo(upshifted); } @@ -213,16 +237,10 @@ namespace VNLib.Net.Http.Core } public void OnPrepare() - { - //Alloc buffer - _buffer = HttpBinBufferPool.Rent(BufferSize); - } + { } public void OnRelease() - { - HttpBinBufferPool.Return(_buffer!); - _buffer = null; - } + { } } }
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/Response/ChunkedStream.cs b/lib/Net.Http/src/Core/Response/ChunkedStream.cs index 724e28d..1b4f7de 100644 --- a/lib/Net.Http/src/Core/Response/ChunkedStream.cs +++ b/lib/Net.Http/src/Core/Response/ChunkedStream.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -36,12 +36,12 @@ using System; using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; using VNLib.Utils; using VNLib.Utils.Memory; +using VNLib.Net.Http.Core.Buffering; #pragma warning disable CA2215 // Dispose methods should call base class dispose @@ -55,27 +55,19 @@ namespace VNLib.Net.Http.Core /// </summary> private sealed class ChunkedStream : Stream, IHttpLifeCycle { - private const string LAST_CHUNK_STRING = "0\r\n\r\n"; - - private readonly ReadOnlyMemory<byte> LastChunk; + private readonly ChunkDataAccumulator ChunckAccumulator; - private readonly Func<Stream> GetTransport; + private readonly IHttpContextInformation ContextInfo; private Stream? TransportStream; private bool HadError; - internal ChunkedStream(Encoding encoding, int chunkBufferSize, Func<Stream> getStream) + internal ChunkedStream(IChunkAccumulatorBuffer buffer, IHttpContextInformation context) { - //Convert and store cached versions of the last chunk bytes - LastChunk = encoding.GetBytes(LAST_CHUNK_STRING); - - //get the min buffer by rounding to the nearest page - int actualBufSize = (int)MemoryUtil.NearestPage(chunkBufferSize); + ContextInfo = context; //Init accumulator - ChunckAccumulator = new(encoding, actualBufSize); - - GetTransport = getStream; + ChunckAccumulator = new(buffer, context); } @@ -91,7 +83,7 @@ namespace VNLib.Net.Http.Core public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; - public override void Write(byte[] buffer, int offset, int count) => Write(new ReadOnlySpan<byte>(buffer, offset, count)); + public override void Write(byte[] buffer, int offset, int count) => Write(buffer.AsSpan(offset, count)); public override void Write(ReadOnlySpan<byte> chunk) { //Only write non-zero chunks @@ -116,7 +108,14 @@ namespace VNLib.Net.Http.Core reader.Advance(written); //Flush accumulator - ChunckAccumulator.Flush(TransportStream!); + Memory<byte> accChunk = ChunckAccumulator.GetChunkData(); + + //Reset the chunk accumulator + ChunckAccumulator.Reset(); + + //Write chunk data + TransportStream!.Write(accChunk.Span); + //Continue to buffer / flush as needed continue; } @@ -161,11 +160,19 @@ namespace VNLib.Net.Http.Core //Advance reader reader.Advance(written); + //Flush accumulator + Memory<byte> accChunk = ChunckAccumulator.GetChunkData(); + + //Reset the chunk accumulator + ChunckAccumulator.Reset(); + //Flush accumulator async - await ChunckAccumulator.FlushAsync(TransportStream!, cancellationToken); + await TransportStream!.WriteAsync(accChunk, cancellationToken); + //Continue to buffer / flush as needed continue; } + break; } while (true); @@ -185,12 +192,15 @@ namespace VNLib.Net.Http.Core { return; } + + //Complete the last chunk + Memory<byte> chunkData = ChunckAccumulator.GetFinalChunkData(); + + //Reset the accumulator + ChunckAccumulator.Reset(); //Write remaining data to stream - await ChunckAccumulator.FlushAsync(TransportStream!, CancellationToken.None); - - //Write final chunk - await TransportStream!.WriteAsync(LastChunk, CancellationToken.None); + await TransportStream!.WriteAsync(chunkData, CancellationToken.None); //Flush base stream await TransportStream!.FlushAsync(CancellationToken.None); @@ -205,12 +215,15 @@ namespace VNLib.Net.Http.Core { return; } - - //Write remaining data to stream - ChunckAccumulator.Flush(TransportStream!); - //Write final chunk - TransportStream!.Write(LastChunk.Span); + //Complete the last chunk + Memory<byte> chunkData = ChunckAccumulator.GetFinalChunkData(); + + //Reset the accumulator + ChunckAccumulator.Reset(); + + //Write chunk data + TransportStream!.Write(chunkData.Span); //Flush base stream TransportStream!.Flush(); @@ -234,7 +247,7 @@ namespace VNLib.Net.Http.Core ChunckAccumulator.OnNewRequest(); //Get transport stream even if not used - TransportStream = GetTransport(); + TransportStream = ContextInfo.GetTransport(); } public void OnComplete() diff --git a/lib/Net.Http/src/Core/Response/HeaderDataAccumulator.cs b/lib/Net.Http/src/Core/Response/HeaderDataAccumulator.cs index c43441c..8c2461c 100644 --- a/lib/Net.Http/src/Core/Response/HeaderDataAccumulator.cs +++ b/lib/Net.Http/src/Core/Response/HeaderDataAccumulator.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -23,135 +23,102 @@ */ using System; -using System.IO; -using System.Text; -using System.Runtime.InteropServices; -using VNLib.Utils; -using VNLib.Utils.IO; using VNLib.Utils.Memory; using VNLib.Utils.Extensions; -using static VNLib.Net.Http.Core.CoreBufferHelpers; +using VNLib.Net.Http.Core.Buffering; namespace VNLib.Net.Http.Core { + internal partial class HttpResponse { - /// <summary> /// Specialized data accumulator for compiling response headers /// </summary> - private sealed class HeaderDataAccumulator : IDataAccumulator<char>, IStringSerializeable, IHttpLifeCycle + private sealed class HeaderDataAccumulator { - private readonly int BufferSize; + private readonly IResponseHeaderAccBuffer _buffer; + private readonly IHttpContextInformation _contextInfo; + private int AccumulatedSize; - public HeaderDataAccumulator(int bufferSize) + public HeaderDataAccumulator(IResponseHeaderAccBuffer accBuffer, IHttpContextInformation ctx) { - //Calc correct char buffer size from bin buffer - this.BufferSize = bufferSize * sizeof(char); + _buffer = accBuffer; + _contextInfo = ctx; } - /* - * May be an issue but wanted to avoid alloc - * if possible since this is a field in a ref - * type - */ - - private UnsafeMemoryHandle<byte>? _handle; - - public void Advance(int count) - { - //Advance writer - AccumulatedSize += count; - } - - public void WriteLine() => this.Append(HttpHelpers.CRLF); - - public void WriteLine(ReadOnlySpan<char> data) + /// <summary> + /// Initializes a new <see cref="ForwardOnlyWriter{T}"/> for buffering character header data + /// </summary> + /// <returns>A <see cref="ForwardOnlyWriter{T}"/> for buffering character header data</returns> + public ForwardOnlyWriter<char> GetWriter() { - this.Append(data); - WriteLine(); + Span<char> chars = _buffer.GetCharSpan(); + return new ForwardOnlyWriter<char>(chars); } - /*Use bin buffers and cast to char buffer*/ - private Span<char> Buffer => MemoryMarshal.Cast<byte, char>(_handle!.Value.Span); - - public int RemainingSize => Buffer.Length - AccumulatedSize; - public Span<char> Remaining => Buffer[AccumulatedSize..]; - public Span<char> Accumulated => Buffer[..AccumulatedSize]; - public int AccumulatedSize { get; set; } - /// <summary> - /// Encodes the buffered data and writes it to the stream, - /// attemts to avoid further allocation where possible + /// Encodes and writes the contents of the <see cref="ForwardOnlyWriter{T}"/> to the internal accumulator /// </summary> - /// <param name="enc"></param> - /// <param name="baseStream"></param> - public void Flush(Encoding enc, Stream baseStream) + /// <param name="writer">The character buffer writer to commit data from</param> + public void CommitChars(ref ForwardOnlyWriter<char> writer) { - ReadOnlySpan<char> span = Accumulated; - //Calc the size of the binary buffer - int byteSize = enc.GetByteCount(span); - //See if there is enough room in the current char buffer - if (RemainingSize < (byteSize / sizeof(char))) - { - //We need to alloc a binary buffer to write data to - using UnsafeMemoryHandle<byte> bin = GetBinBuffer(byteSize, false); - //encode data - int encoded = enc.GetBytes(span, bin.Span); - //Write to stream - baseStream.Write(bin.Span[..encoded]); - } - else + if (writer.Written == 0) { - //Get bin buffer by casting remaining accumulator buffer - Span<byte> bin = MemoryMarshal.Cast<char, byte>(Remaining); - //encode data - int encoded = enc.GetBytes(span, bin); - //Write to stream - baseStream.Write(bin[..encoded]); + return; } - Reset(); - } - - public void Reset() => AccumulatedSize = 0; - + //Write the entire token to the buffer + WriteToken(writer.AsSpan()); + } - public void OnPrepare() + /// <summary> + /// Encodes a single token and writes it directly to the internal accumulator + /// </summary> + /// <param name="chars">The character sequence to accumulate</param> + public void WriteToken(ReadOnlySpan<char> chars) { - //Alloc buffer - _handle = GetBinBuffer(BufferSize, false); - } + //Get remaining buffer + Span<byte> remaining = _buffer.GetBinSpan()[AccumulatedSize..]; - public void OnRelease() + //Commit all chars to the buffer + AccumulatedSize += _contextInfo.Encoding.GetBytes(chars, remaining); + } + + /// <summary> + /// Writes the http termination sequence to the internal accumulator + /// </summary> + public void WriteTermination() { - _handle!.Value.Dispose(); - _handle = null; + //Write the http termination sequence + Span<byte> remaining = _buffer.GetBinSpan()[AccumulatedSize..]; + + _contextInfo.CrlfBytes.Span.CopyTo(remaining); + + //Advance the accumulated window + AccumulatedSize += _contextInfo.CrlfBytes.Length; } - public void OnNewRequest() - {} + /// <summary> + /// Resets the internal accumulator + /// </summary> + public void Reset() => AccumulatedSize = 0; - public void OnComplete() + /// <summary> + /// Gets the accumulated response data as its memory buffer, and resets the internal accumulator + /// </summary> + /// <returns>The buffer segment containing the accumulated response data</returns> + public Memory<byte> GetResponseData() { - Reset(); - } + //get the current buffer as memory and return the accumulated segment + Memory<byte> accumulated = _buffer.GetMemory()[..AccumulatedSize]; + //Reset the buffer + Reset(); - ///<inheritdoc/> - public string Compile() => Accumulated.ToString(); - ///<inheritdoc/> - public void Compile(ref ForwardOnlyWriter<char> writer) => writer.Append(Accumulated); - ///<inheritdoc/> - public ERRNO Compile(in Span<char> buffer) - { - ForwardOnlyWriter<char> writer = new(buffer); - Compile(ref writer); - return writer.Written; + return accumulated; } - ///<inheritdoc/> - public override string ToString() => Compile(); } } }
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs b/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs index b03363e..6bc92ff 100644 --- a/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs +++ b/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -23,13 +23,13 @@ */ using System; -using System.Buffers; -using System.IO.Compression; using System.IO; +using System.IO.Compression; using System.Net; using System.Threading; using System.Threading.Tasks; + namespace VNLib.Net.Http.Core { @@ -43,7 +43,7 @@ namespace VNLib.Net.Http.Core * and the release method will be called so the context can be reused */ - ValueTask discardTask = Request.InputStream.DiscardRemainingAsync(ParentServer.Config.DiscardBufferSize); + ValueTask discardTask = Request.InputStream.DiscardRemainingAsync(Buffers); //See if discard is needed if (ResponseBody.HasData) @@ -99,7 +99,7 @@ namespace VNLib.Net.Http.Core Response.Headers[HttpResponseHeader.ContentLength] = ResponseBody.Length.ToString(); } - //We must send headers here so content length doesnt get overwritten + //We must send headers here so content length doesnt get overwritten, close will be called after this to flush to transport Response.FlushHeaders(); } else @@ -125,7 +125,7 @@ namespace VNLib.Net.Http.Core Response.SetContentRange(range.Item1, endRange, length); //Get the raw output stream and set the length to the number of bytes - outputStream = Response.GetStream(length); + outputStream = await Response.GetStreamAsync(length); await WriteEntityDataAsync(outputStream, length, token); } @@ -150,8 +150,10 @@ namespace VNLib.Net.Http.Core { //Specify gzip encoding (using chunked encoding) Response.Headers[HttpResponseHeader.ContentEncoding] = "gzip"; + //get the chunked output stream - Stream chunked = Response.GetStream(); + Stream chunked = await Response.GetStreamAsync(); + //Use chunked encoding and send data as its written outputStream = new GZipStream(chunked, ParentServer.Config.CompressionLevel, false); } @@ -161,7 +163,7 @@ namespace VNLib.Net.Http.Core //Specify gzip encoding (using chunked encoding) Response.Headers[HttpResponseHeader.ContentEncoding] = "deflate"; //get the chunked output stream - Stream chunked = Response.GetStream(); + Stream chunked = await Response.GetStreamAsync(); //Use chunked encoding and send data as its written outputStream = new DeflateStream(chunked, ParentServer.Config.CompressionLevel, false); } @@ -171,7 +173,7 @@ namespace VNLib.Net.Http.Core //Specify Brotli encoding (using chunked encoding) Response.Headers[HttpResponseHeader.ContentEncoding] = "br"; //get the chunked output stream - Stream chunked = Response.GetStream(); + Stream chunked = await Response.GetStreamAsync(); //Use chunked encoding and send data as its written outputStream = new BrotliStream(chunked, ParentServer.Config.CompressionLevel, false); } @@ -180,7 +182,7 @@ namespace VNLib.Net.Http.Core case HttpRequestExtensions.CompressionType.None: default: //Since we know how long the response will be, we can submit it now (see note above for same issues) - outputStream = Response.GetStream(ResponseBody.Length); + outputStream = await Response.GetStreamAsync(ResponseBody.Length); break; } @@ -197,14 +199,11 @@ namespace VNLib.Net.Http.Core //Determine if buffer is required if (ResponseBody.BufferRequired) { - //Calc a buffer size (always a safe cast since rbs is an integer) - int bufferSize = (int)Math.Min((long)ParentServer.Config.ResponseBufferSize, ResponseBody.Length); - - //Alloc buffer, and dispose when completed - using IMemoryOwner<byte> buffer = CoreBufferHelpers.GetMemory(bufferSize, false); + //Get response data buffer, may be smaller than suggested size + Memory<byte> buffer = Buffers.GetResponseDataBuffer(); //Write response - await ResponseBody.WriteEntityAsync(outputStream, buffer.Memory, token); + await ResponseBody.WriteEntityAsync(outputStream, buffer, token); } //No buffer is required, write response directly else @@ -227,14 +226,11 @@ namespace VNLib.Net.Http.Core //Determine if buffer is required if (ResponseBody.BufferRequired) { - //Calc a buffer size (always a safe cast since rbs is an integer) - int bufferSize = (int)Math.Min((long)ParentServer.Config.ResponseBufferSize, ResponseBody.Length); - - //Alloc buffer, and dispose when completed - using IMemoryOwner<byte> buffer = CoreBufferHelpers.GetMemory(bufferSize, false); + //Get response data buffer, may be smaller than suggested size + Memory<byte> buffer = Buffers.GetResponseDataBuffer(); //Write response - await ResponseBody.WriteEntityAsync(outputStream, length, buffer.Memory, token); + await ResponseBody.WriteEntityAsync(outputStream, length, buffer, token); } //No buffer is required, write response directly else diff --git a/lib/Net.Http/src/Core/Response/HttpResponse.cs b/lib/Net.Http/src/Core/Response/HttpResponse.cs index 69ef2f1..9093006 100644 --- a/lib/Net.Http/src/Core/Response/HttpResponse.cs +++ b/lib/Net.Http/src/Core/Response/HttpResponse.cs @@ -25,25 +25,26 @@ using System; using System.IO; using System.Net; -using System.Text; using System.Threading.Tasks; using System.Collections.Generic; using System.Runtime.CompilerServices; +using VNLib.Utils; using VNLib.Utils.IO; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; +using VNLib.Net.Http.Core.Buffering; namespace VNLib.Net.Http.Core { - internal partial class HttpResponse : IHttpLifeCycle + internal partial class HttpResponse : IHttpLifeCycle, IStringSerializeable { private readonly HashSet<HttpCookie> Cookies; private readonly HeaderDataAccumulator Writer; private readonly DirectStream ReusableDirectStream; private readonly ChunkedStream ReusableChunkedStream; - private readonly Func<Stream> _getStream; - private readonly Encoding ResponseEncoding; - private readonly Func<HttpVersion> GetVersion; + private readonly IHttpContextInformation ContextInfo; private bool HeadersSent; private bool HeadersBegun; @@ -55,24 +56,22 @@ namespace VNLib.Net.Http.Core /// </summary> public VnWebHeaderCollection Headers { get; } - public HttpResponse(Encoding encoding, int headerBufferSize, int chunkedBufferSize, Func<Stream> getStream, Func<HttpVersion> getVersion) + public HttpResponse(IHttpBufferManager manager, IHttpContextInformation ctx) { + ContextInfo = ctx; + //Initialize a new header collection and a cookie jar Headers = new(); Cookies = new(); - //Create a new reusable writer stream - Writer = new(headerBufferSize); - _getStream = getStream; - ResponseEncoding = encoding; + //Create a new reusable writer stream + Writer = new(manager.ResponseHeaderBuffer, ctx); //Create a new chunked stream - ReusableChunkedStream = new(encoding, chunkedBufferSize, getStream); + ReusableChunkedStream = new(manager.ChunkAccumulatorBuffer, ctx); ReusableDirectStream = new(); - GetVersion = getVersion; } - /// <summary> /// Sets the status code of the response /// </summary> @@ -101,18 +100,27 @@ namespace VNLib.Net.Http.Core internal async Task SendEarly100ContinueAsync() { Check(); + //Send a status message with the continue response status - Writer.WriteLine(HttpHelpers.GetResponseString(GetVersion(), HttpStatusCode.Continue)); + Writer.WriteToken(HttpHelpers.GetResponseString(ContextInfo.CurrentVersion, HttpStatusCode.Continue)); + //Trailing crlf - Writer.WriteLine(); + Writer.WriteTermination(); + + //Get the response data header block + Memory<byte> responseBlock = Writer.GetResponseData(); + //get base stream - Stream bs = _getStream(); - //Flush writer to stream (will reset the buffer) - Writer.Flush(ResponseEncoding, bs); + Stream bs = ContextInfo.GetTransport(); + + //Write the response data to the base stream + await bs.WriteAsync(responseBlock); + //Flush the base stream await bs.FlushAsync(); } + /// <summary> /// Sends the status message and all available headers to the client. /// Headers set after method returns will be sent when output stream is requested or scope exits @@ -122,53 +130,76 @@ namespace VNLib.Net.Http.Core public void FlushHeaders() { Check(); - //If headers havent been sent yet, start with status code + + //Get a fresh writer to buffer character data + ForwardOnlyWriter<char> writer = Writer.GetWriter(); + + //If headers havent been sent yet, start with status line if (!HeadersBegun) { //write status code first - Writer.WriteLine(HttpHelpers.GetResponseString(GetVersion(), _code)); + writer.Append(HttpHelpers.GetResponseString(ContextInfo.CurrentVersion, _code)); + writer.Append(HttpHelpers.CRLF); //Write the date to header buffer - Writer.Append("Date: "); - Writer.Append(DateTimeOffset.UtcNow, "R"); - Writer.WriteLine(); + writer.Append("Date: "); + writer.Append(DateTimeOffset.UtcNow, "R"); + writer.Append(HttpHelpers.CRLF); + //Set begun flag HeadersBegun = true; } + //Write headers for (int i = 0; i < Headers.Count; i++) { - Writer.Append(Headers.Keys[i]); //Write header key - Writer.Append(": "); //Write separator - Writer.WriteLine(Headers[i]); //Write the header value + writer.Append(Headers.Keys[i]); //Write header key + writer.Append(": "); //Write separator + writer.Append(Headers[i]); //Write the header value + writer.Append(HttpHelpers.CRLF); //Crlf } + //Remove writen headers Headers.Clear(); + //Write cookies if any are set if (Cookies.Count > 0) { - //Write cookies if any have been set + //Enumerate and write foreach (HttpCookie cookie in Cookies) { - Writer.Append("Set-Cookie: "); - Writer.Append(in cookie); - Writer.WriteLine(); + writer.Append("Set-Cookie: "); + + //Write the cookie to the header buffer + cookie.Compile(ref writer); + + writer.Append(HttpHelpers.CRLF); } + //Clear all current cookies Cookies.Clear(); } + + //Commit headers + Writer.CommitChars(ref writer); } - private void EndFlushHeaders(Stream transport) + + private ValueTask EndFlushHeadersAsync(Stream transport) { //Sent all available headers FlushHeaders(); + //Last line to end headers - Writer.WriteLine(); - - //Flush writer - Writer.Flush(ResponseEncoding, transport); + Writer.WriteTermination(); + + //Get the response data header block + Memory<byte> responseBlock = Writer.GetResponseData(); + //Update sent headers HeadersSent = true; + + //Write the response data to the base stream + return responseBlock.IsEmpty ? ValueTask.CompletedTask : transport.WriteAsync(responseBlock); } /// <summary> @@ -178,14 +209,17 @@ namespace VNLib.Net.Http.Core /// <returns>A <see cref="Stream"/> configured for writing data to client</returns> /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="InvalidOperationException"></exception> - public Stream GetStream(long ContentLength) + public async ValueTask<Stream> GetStreamAsync(long ContentLength) { Check(); + //Add content length header Headers[HttpResponseHeader.ContentLength] = ContentLength.ToString(); + //End sending headers so the user can write to the ouput stream - Stream transport = _getStream(); - EndFlushHeaders(transport); + Stream transport = ContextInfo.GetTransport(); + + await EndFlushHeadersAsync(transport); //Init direct stream ReusableDirectStream.Prepare(transport); @@ -200,21 +234,24 @@ namespace VNLib.Net.Http.Core /// <returns><see cref="Stream"/> supporting chunked encoding</returns> /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="InvalidOperationException"></exception> - public Stream GetStream() + public async ValueTask<Stream> GetStreamAsync() { #if DEBUG - if (GetVersion() != HttpVersion.Http11) + if (ContextInfo.CurrentVersion != HttpVersion.Http11) { throw new InvalidOperationException("Chunked transfer encoding is not acceptable for this http version"); } #endif Check(); + //Set encoding type to chunked with user-defined compression Headers[HttpResponseHeader.TransferEncoding] = "chunked"; + //End sending headers so the user can write to the ouput stream - Stream transport = _getStream(); - EndFlushHeaders(transport); - + Stream transport = ContextInfo.GetTransport(); + + await EndFlushHeadersAsync(transport); + //Return the reusable stream return ReusableChunkedStream; } @@ -236,7 +273,7 @@ namespace VNLib.Net.Http.Core internal async ValueTask CloseAsync() { //If headers havent been sent yet, send them and there must be no content - if (!HeadersBegun) + if (!HeadersBegun || !HeadersSent) { //RFC 7230, length only set on 200 + but not 204 if ((int)_code >= 200 && (int)_code != 204) @@ -244,24 +281,13 @@ namespace VNLib.Net.Http.Core //If headers havent been sent by this stage there is no content, so set length to 0 Headers[HttpResponseHeader.ContentLength] = "0"; } + //Flush transport - Stream transport = _getStream(); - EndFlushHeaders(transport); - //Flush transport - await transport.FlushAsync(); - } - //Headers have been started but not finished yet - else if (!HeadersSent) - { - //RFC 7230, length only set on 200 + but not 204 - if ((int)_code >= 200 && (int)_code != 204) - { - //If headers havent been sent by this stage there is no content, so set length to 0 - Headers[HttpResponseHeader.ContentLength] = "0"; - } - //If headers arent done sending yet, conclude headers - Stream transport = _getStream(); - EndFlushHeaders(transport); + Stream transport = ContextInfo.GetTransport(); + + //Finalize headers + await EndFlushHeadersAsync(transport); + //Flush transport await transport.FlushAsync(); } @@ -271,13 +297,11 @@ namespace VNLib.Net.Http.Core public void OnPrepare() { //Propagate all child lifecycle hooks - Writer.OnPrepare(); ReusableChunkedStream.OnPrepare(); } public void OnRelease() { - Writer.OnRelease(); ReusableChunkedStream.OnRelease(); } @@ -286,7 +310,6 @@ namespace VNLib.Net.Http.Core //Default to okay status code _code = HttpStatusCode.OK; - Writer.OnNewRequest(); ReusableChunkedStream.OnNewRequest(); } @@ -299,9 +322,74 @@ namespace VNLib.Net.Http.Core HeadersBegun = false; HeadersSent = false; + //Reset header writer + Writer.Reset(); + //Call child lifecycle hooks - Writer.OnComplete(); ReusableChunkedStream.OnComplete(); } + +#if DEBUG + + public override string ToString() => Compile(); + + public string Compile() + { + //Alloc char buffer + using IMemoryHandle<char> buffer = MemoryUtil.SafeAlloc<char>(16 * 1024); + + //Writer + ForwardOnlyWriter<char> writer = new (buffer.Span); + Compile(ref writer); + return writer.ToString(); + } + + public void Compile(ref ForwardOnlyWriter<char> writer) + { + /* READONLY!!! */ + + //Status line + writer.Append(HttpHelpers.GetResponseString(ContextInfo.CurrentVersion, _code)); + writer.Append(HttpHelpers.CRLF); + writer.Append("Date: "); + writer.Append(DateTimeOffset.UtcNow, "R"); + writer.Append(HttpHelpers.CRLF); + + //Write headers + for (int i = 0; i < Headers.Count; i++) + { + writer.Append(Headers.Keys[i]); //Write header key + writer.Append(": "); //Write separator + writer.Append(Headers[i]); //Write the header value + writer.Append(HttpHelpers.CRLF); //Crlf + } + + //Enumerate and write + foreach (HttpCookie cookie in Cookies) + { + writer.Append("Set-Cookie: "); + + //Write the cookie to the header buffer + cookie.Compile(ref writer); + + writer.Append(HttpHelpers.CRLF); + } + + //Last line to end headers + writer.Append(HttpHelpers.CRLF); + } + + public ERRNO Compile(in Span<char> buffer) + { + ForwardOnlyWriter<char> writer = new(buffer); + Compile(ref writer); + return writer.Written; + } +#else + + public override string ToString() => "HTTP Library was compiled without a DEBUG directive, response logging is not available"; + +#endif + } }
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/SharedHeaderReaderBuffer.cs b/lib/Net.Http/src/Core/SharedHeaderReaderBuffer.cs deleted file mode 100644 index fb20e68..0000000 --- a/lib/Net.Http/src/Core/SharedHeaderReaderBuffer.cs +++ /dev/null @@ -1,86 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Net.Http -* File: SharedHeaderReaderBuffer.cs -* -* SharedHeaderReaderBuffer.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.Runtime.InteropServices; - -using VNLib.Utils.Memory; - -namespace VNLib.Net.Http.Core -{ - sealed class SharedHeaderReaderBuffer : IHttpLifeCycle - { - private UnsafeMemoryHandle<byte>? Handle; - - /// <summary> - /// The size of the binary buffer - /// </summary> - public int BinLength { get; } - - private readonly int _bufferSize; - - internal SharedHeaderReaderBuffer(int length) - { - _bufferSize = length + (length * sizeof(char)); - - //Round to nearest page - _bufferSize = (int)MemoryUtil.NearestPage(_bufferSize); - - //Bin buffer is the specified size - BinLength = length; - } - - /// <summary> - /// The binary buffer to store reader information - /// </summary> - public Span<byte> BinBuffer => Handle!.Value.Span[..BinLength]; - - /// <summary> - /// The char buffer to store read characters in - /// </summary> - public Span<char> CharBuffer => MemoryMarshal.Cast<byte, char>(Handle!.Value.Span[BinLength..]); - - public void OnPrepare() - { - //Alloc the shared buffer - Handle = CoreBufferHelpers.GetBinBuffer(_bufferSize, true); - } - - public void OnRelease() - { - //Free buffer - Handle?.Dispose(); - Handle = null; - } - - public void OnNewRequest() - {} - - public void OnComplete() - { - //Zero buffer - Handle!.Value.Span.Clear(); - } - } -}
\ No newline at end of file diff --git a/lib/Net.Http/src/Helpers/TransportReader.cs b/lib/Net.Http/src/Core/TransportReader.cs index 722120b..58c23df 100644 --- a/lib/Net.Http/src/Helpers/TransportReader.cs +++ b/lib/Net.Http/src/Core/TransportReader.cs @@ -28,6 +28,7 @@ using System.Text; using VNLib.Utils; using VNLib.Utils.IO; +using VNLib.Net.Http.Core.Buffering; namespace VNLib.Net.Http.Core { @@ -39,12 +40,14 @@ namespace VNLib.Net.Http.Core { ///<inheritdoc/> public readonly Encoding Encoding { get; } + ///<inheritdoc/> public readonly ReadOnlyMemory<byte> LineTermination { get; } + ///<inheritdoc/> public readonly Stream BaseStream { get; } - private readonly SharedHeaderReaderBuffer BinBuffer; + private readonly IHttpHeaderParseBuffer Buffer; private int BufWindowStart; private int BufWindowEnd; @@ -56,35 +59,39 @@ namespace VNLib.Net.Http.Core /// <param name="buffer">The shared binary buffer</param> /// <param name="encoding">The encoding to use when reading bianry</param> /// <param name="lineTermination">The line delimiter to search for</param> - public TransportReader(Stream transport, SharedHeaderReaderBuffer buffer, Encoding encoding, ReadOnlyMemory<byte> lineTermination) + public TransportReader(Stream transport, IHttpHeaderParseBuffer buffer, Encoding encoding, ReadOnlyMemory<byte> lineTermination) { BufWindowEnd = 0; BufWindowStart = 0; Encoding = encoding; BaseStream = transport; LineTermination = lineTermination; - BinBuffer = buffer; + Buffer = buffer; } ///<inheritdoc/> public readonly int Available => BufWindowEnd - BufWindowStart; ///<inheritdoc/> - public readonly Span<byte> BufferedDataWindow => BinBuffer.BinBuffer[BufWindowStart..BufWindowEnd]; + public readonly Span<byte> BufferedDataWindow => Buffer.GetBinSpan()[BufWindowStart..BufWindowEnd]; ///<inheritdoc/> public void Advance(int count) => BufWindowStart += count; + ///<inheritdoc/> public void FillBuffer() { //Get a buffer from the end of the current window to the end of the buffer - Span<byte> bufferWindow = BinBuffer.BinBuffer[BufWindowEnd..]; + Span<byte> bufferWindow = Buffer.GetBinSpan()[BufWindowEnd..]; + //Read from stream int read = BaseStream.Read(bufferWindow); + //Update the end of the buffer window to the end of the read data BufWindowEnd += read; } + ///<inheritdoc/> public ERRNO CompactBufferWindow() { @@ -92,18 +99,23 @@ namespace VNLib.Net.Http.Core if (BufWindowStart > 0) { //Get span over engire buffer - Span<byte> buffer = BinBuffer.BinBuffer; + Span<byte> buffer = Buffer.GetBinSpan(); + //Get data within window Span<byte> usedData = buffer[BufWindowStart..BufWindowEnd]; + //Copy remaining to the begining of the buffer usedData.CopyTo(buffer); + //Buffer window start is 0 BufWindowStart = 0; + //Buffer window end is now the remaining size BufWindowEnd = usedData.Length; } - //Return the number of bytes of available space - return BinBuffer.BinLength - BufWindowEnd; + + //Return the number of bytes of available space from the end of the current window + return Buffer.BinSize - BufWindowEnd; } } } diff --git a/lib/Net.Http/src/FileUpload.cs b/lib/Net.Http/src/FileUpload.cs index 654d682..794c623 100644 --- a/lib/Net.Http/src/FileUpload.cs +++ b/lib/Net.Http/src/FileUpload.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -24,95 +24,33 @@ using System; using System.IO; -using System.Text; - -using VNLib.Utils.IO; -using VNLib.Utils.Memory; -using VNLib.Utils.Extensions; - -using static VNLib.Net.Http.Core.CoreBufferHelpers; namespace VNLib.Net.Http { /// <summary> /// Represents an file that was received as an entity body, either using Multipart/FormData or as the entity body itself /// </summary> - public readonly struct FileUpload + /// <param name="ContentType"> + /// Content type of uploaded file + /// </param> + /// <param name="FileData"> + /// The file data captured on upload + /// </param> + /// <param name="FileName"> + /// Name of file uploaded + /// </param> + /// <param name="DisposeStream"> + /// A value that indicates whether the stream should be disposed when the handle is freed + /// </param> + public readonly record struct FileUpload(Stream FileData, bool DisposeStream, ContentType ContentType, string? FileName) { /// <summary> - /// Content type of uploaded file - /// </summary> - public readonly ContentType ContentType; - /// <summary> - /// Name of file uploaded - /// </summary> - public readonly string FileName; - /// <summary> - /// The file data captured on upload - /// </summary> - public readonly Stream FileData; - - private readonly bool OwnsHandle; - - /// <summary> - /// Allocates a new binary buffer, encodes, and copies the specified data to a new <see cref="FileUpload"/> - /// structure of the specified content type - /// </summary> - /// <param name="data">The string data to copy</param> - /// <param name="dataEncoding">The encoding instance to encode the string data from</param> - /// <param name="filename">The name of the file</param> - /// <param name="ct">The content type of the file data</param> - /// <returns>The <see cref="FileUpload"/> container</returns> - internal static FileUpload FromString(ReadOnlySpan<char> data, Encoding dataEncoding, string filename, ContentType ct) - { - //get number of bytes - int bytes = dataEncoding.GetByteCount(data); - //get a buffer from the HTTP heap - MemoryHandle<byte> buffHandle = HttpPrivateHeap.Alloc<byte>(bytes); - try - { - //Convert back to binary - bytes = dataEncoding.GetBytes(data, buffHandle); - - //Create a new memory stream encapsulating the file data - VnMemoryStream vms = VnMemoryStream.ConsumeHandle(buffHandle, bytes, true); - - //Create new upload wrapper - return new (vms, filename, ct, true); - } - catch - { - //Make sure the hanle gets disposed if there is an error - buffHandle.Dispose(); - throw; - } - } - - /// <summary> - /// Initialzes a new <see cref="FileUpload"/> structure from the specified data - /// and file information. - /// </summary> - /// <param name="data"></param> - /// <param name="filename"></param> - /// <param name="ct"></param> - /// <param name="ownsHandle"></param> - public FileUpload(Stream data, string filename, ContentType ct, bool ownsHandle) - { - FileName = filename; - ContentType = ct; - //Store handle ownership - OwnsHandle = ownsHandle; - //Store the stream - FileData = data; - } - - /// <summary> - /// Releases any memory the current instance holds if it owns the handles + /// Disposes the stream if the handle is owned /// </summary> - internal readonly void Free() + public readonly void Free() { //Dispose the handle if we own it - if (OwnsHandle) + if (DisposeStream) { //This should always be synchronous FileData.Dispose(); diff --git a/lib/Net.Http/src/Helpers/CoreBufferHelpers.cs b/lib/Net.Http/src/Helpers/CoreBufferHelpers.cs index cdfeac7..e3e5854 100644 --- a/lib/Net.Http/src/Helpers/CoreBufferHelpers.cs +++ b/lib/Net.Http/src/Helpers/CoreBufferHelpers.cs @@ -22,23 +22,11 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -/* - * This class is meant to provide memory helper methods - * as a centralized HTTP local memory api. - * - * Pools and heaps are privatized to help avoid - * leaking sensitive HTTP data across other application - * allocations and help provide memory optimization. - */ using System; using System.Buffers; -using System.Security; -using System.Threading; using VNLib.Utils.IO; -using VNLib.Utils.Memory; -using VNLib.Utils.Extensions; namespace VNLib.Net.Http.Core { @@ -54,71 +42,6 @@ namespace VNLib.Net.Http.Core public static ArrayPool<byte> HttpBinBufferPool { get; } = ArrayPool<byte>.Create(); /// <summary> - /// An <see cref="IUnmangedHeap"/> used for internal HTTP buffers - /// </summary> - public static IUnmangedHeap HttpPrivateHeap => _lazyHeap.Value; - - private static readonly Lazy<IUnmangedHeap> _lazyHeap = new(MemoryUtil.InitializeNewHeapForProcess, LazyThreadSafetyMode.PublicationOnly); - - /// <summary> - /// Alloctes an unsafe block of memory from the internal heap, or buffer pool - /// </summary> - /// <param name="size">The number of elemnts to allocate</param> - /// <param name="zero">A value indicating of the block should be zeroed before returning</param> - /// <returns>A handle to the block of memory</returns> - /// <exception cref="SecurityException"></exception> - /// <exception cref="OutOfMemoryException"></exception> - public static UnsafeMemoryHandle<byte> GetBinBuffer(int size, bool zero) - { - //Calc buffer size to the nearest page size - size = (int)MemoryUtil.NearestPage(size); - - /* - * Heap synchronziation may be enabled for our private heap, so we may want - * to avoid it in favor of performance over private heap segmentation. - * - * If synchronization is enabled, use the system heap - */ - - if ((HttpPrivateHeap.CreationFlags & HeapCreation.UseSynchronization) > 0) - { - return MemoryUtil.UnsafeAlloc(size, zero); - } - else - { - return HttpPrivateHeap.UnsafeAlloc<byte>(size, zero); - } - } - - public static IMemoryOwner<byte> GetMemory(int size, bool zero) - { - //Calc buffer size to the nearest page size - size = (int)MemoryUtil.NearestPage(size); - - /* - * Heap synchronziation may be enabled for our private heap, so we may want - * to avoid it in favor of performance over private heap segmentation. - * - * If synchronization is enabled, use the system heap - */ - - if ((HttpPrivateHeap.CreationFlags & HeapCreation.UseSynchronization) > 0) - { - return MemoryUtil.Shared.DirectAlloc<byte>(size, zero); - } - //If the block is larger than an safe array size, avoid LOH pressure - else if(size > MemoryUtil.MAX_UNSAFE_POOL_SIZE) - { - return HttpPrivateHeap.DirectAlloc<byte>(size, zero); - } - //Use the array pool to get a memory handle - else - { - return new VnTempBuffer<byte>(HttpBinBufferPool, size, zero); - } - } - - /// <summary> /// Gets the remaining data in the reader buffer and prepares a /// sliding window buffer to read data from /// </summary> diff --git a/lib/Net.Http/src/Helpers/HttpHelpers.cs b/lib/Net.Http/src/Helpers/HttpHelpers.cs index 0937981..d5c471f 100644 --- a/lib/Net.Http/src/Helpers/HttpHelpers.cs +++ b/lib/Net.Http/src/Helpers/HttpHelpers.cs @@ -53,6 +53,7 @@ namespace VNLib.Net.Http /// Extended <see cref="HttpRequestHeader"/> for origin header, DO NOT USE IN <see cref="WebHeaderCollection"/> /// </summary> internal const HttpRequestHeader Origin = (HttpRequestHeader)42; + /// <summary> /// Extended <see cref="HttpRequestHeader"/> for Content-Disposition, DO NOT USE IN <see cref="WebHeaderCollection"/> /// </summary> @@ -72,7 +73,7 @@ namespace VNLib.Net.Http * enum value (with some extra support) */ - private static readonly IReadOnlyDictionary<string, HttpRequestHeader> RequestHeaderLookup = new Dictionary<string, HttpRequestHeader>() + private static readonly IReadOnlyDictionary<string, HttpRequestHeader> RequestHeaderLookup = new Dictionary<string, HttpRequestHeader>(StringComparer.OrdinalIgnoreCase) { {"CacheControl", HttpRequestHeader.CacheControl }, {"Connection", HttpRequestHeader.Connection }, @@ -191,6 +192,7 @@ namespace VNLib.Net.Http * Pre-compute common headers */ Dictionary<int, HttpRequestHeader> requestHeaderHashes = new(); + //Add all HTTP methods foreach (string headerValue in RequestHeaderLookup.Keys) { @@ -199,6 +201,7 @@ namespace VNLib.Net.Http //Store the http header enum value with the hash-code of the string of said header requestHeaderHashes[hashCode] = RequestHeaderLookup[headerValue]; } + RequestHeaderHashLookup = requestHeaderHashes; } } @@ -211,12 +214,14 @@ namespace VNLib.Net.Http /// <returns>Http acceptable string representing a content type</returns> /// <exception cref="KeyNotFoundException"></exception> public static string GetContentTypeString(ContentType type) => CtToMime[type]; + /// <summary> /// Returns the <see cref="ContentType"/> enum value from the MIME string /// </summary> /// <param name="type">Content type from request</param> /// <returns><see cref="ContentType"/> of request, <see cref="ContentType.NonSupported"/> if unknown</returns> public static ContentType GetContentType(string type) => MimeToCt.GetValueOrDefault(type, ContentType.NonSupported); + //Cache control string using mdn reference //https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control /// <summary> @@ -260,6 +265,7 @@ namespace VNLib.Net.Http sb.Append(maxAge); return sb.ToString(); } + /// <summary> /// Builds a Cache-Control MIME content header from the specified flags /// </summary> @@ -268,6 +274,7 @@ namespace VNLib.Net.Http /// <param name="immutable">Sets the immutable argument</param> /// <returns>The string representation of the Cache-Control header</returns> public static string GetCacheString(CacheType type, TimeSpan maxAge, bool immutable = false) => GetCacheString(type, (int)maxAge.TotalSeconds, immutable); + /// <summary> /// Returns an enum value of an httpmethod of an http request method string /// </summary> @@ -281,6 +288,7 @@ namespace VNLib.Net.Http //run the lookup and return not supported if the method was not found return MethodHashLookup.GetValueOrDefault(hashCode, HttpMethod.None); } + /// <summary> /// Compares the first 3 bytes of IPV4 ip address or the first 6 bytes of a IPV6. Can be used to determine if the address is local to another address /// </summary> @@ -324,6 +332,7 @@ namespace VNLib.Net.Http } return false; } + /// <summary> /// Selects a <see cref="ContentType"/> for a given file extension /// </summary> @@ -338,6 +347,7 @@ namespace VNLib.Net.Http //If the extension is defined, perform a lookup, otherwise return the default return ExtensionToCt.GetValueOrDefault(extention.ToString(), ContentType.Binary); } + /// <summary> /// Selects a runtime compiled <see cref="string"/> matching the given <see cref="HttpStatusCode"/> and <see cref="HttpVersion"/> /// </summary> @@ -395,17 +405,22 @@ namespace VNLib.Net.Http { //First parameter should be the type argument type = header.SliceBeforeParam(';').Trim().ToString(); + //Set defaults for name and filename name = fileName = null; + //get the name parameter ReadOnlySpan<char> nameSpan = header.SliceAfterParam("name=\""); + if (!nameSpan.IsEmpty) { //Capture the name parameter value and trim it up name = nameSpan.SliceBeforeParam('"').Trim().ToString(); } + //Check for the filename parameter ReadOnlySpan<char> fileNameSpan = header.SliceAfterParam("filename=\""); + if (!fileNameSpan.IsEmpty) { //Capture the name parameter value and trim it up diff --git a/lib/Net.Http/src/HttpBufferConfig.cs b/lib/Net.Http/src/HttpBufferConfig.cs new file mode 100644 index 0000000..d4dc0f4 --- /dev/null +++ b/lib/Net.Http/src/HttpBufferConfig.cs @@ -0,0 +1,82 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpBufferConfig.cs +* +* HttpBufferConfig.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 +{ + /// <summary> + /// Holds configuration constants for http protocol buffers + /// </summary> + public readonly record struct HttpBufferConfig() + { + /// <summary> + /// The maximum buffer size to use when parsing Multi-part/Form-data file uploads + /// </summary> + /// <remarks> + /// This value is used to create the buffer used to read data from the input stream + /// into memory for parsing. Form-data uploads must be parsed in memory because + /// the data is not delimited by a content length. + /// </remarks> + public readonly int FormDataBufferSize { get; init; } = 8192; + + /// <summary> + /// The buffer size used to read HTTP headers from the transport. + /// </summary> + /// <remarks> + /// Setting this value too low will result in header parsing failures + /// and 400 Bad Request responses. Setting it too high can result in + /// resource abuse or high memory usage. 8k is usually a good value. + /// </remarks> + public readonly int RequestHeaderBufferSize { get; init; } = 8192; + + /// <summary> + /// The size (in bytes) of the http response header accumulator buffer. + /// </summary> + /// <remarks> + /// Http responses use an internal accumulator to buffer all response headers + /// before writing them to the transport in on write operation. If this value + /// is too low, the response will fail to write. If it is too high, it + /// may cause resource exhaustion or high memory usage. + /// </remarks> + public readonly int ResponseHeaderBufferSize { get; init; } = 16 * 1024; + + /// <summary> + /// The size (in bytes) of the buffer to use to discard unread request entity bodies + /// </summary> + public readonly int DiscardBufferSize { get; init; } = 64 * 1024; + + /// <summary> + /// The size of the buffer to use when writing response data to the transport + /// </summary> + /// <remarks> + /// This value is the size of the buffer used to copy data from the response + /// entity stream, to the transport stream. + /// </remarks> + public readonly int ResponseBufferSize { get; init; } = 32 * 1024; + + /// <summary> + /// The size of the buffer used to accumulate chunked response data before writing to the transport + /// </summary> + public readonly int ChunkedResponseAccumulatorSize { get; init; } = 64 * 1024; + } +}
\ No newline at end of file diff --git a/lib/Net.Http/src/HttpConfig.cs b/lib/Net.Http/src/HttpConfig.cs index 8e73176..6e22fea 100644 --- a/lib/Net.Http/src/HttpConfig.cs +++ b/lib/Net.Http/src/HttpConfig.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -33,122 +33,92 @@ namespace VNLib.Net.Http /// <summary> /// Represents configration variables used to create the instance and manage http connections /// </summary> - public readonly struct HttpConfig + /// <param name="ServerLog"> + /// A log provider that all server related log entiries will be written to + /// </param> + /// <param name="MemoryPool"> + /// Server memory pool to use for allocating buffers + /// </param> + public readonly record struct HttpConfig(ILogProvider ServerLog, IHttpMemoryPool MemoryPool) { - public HttpConfig(ILogProvider log) - { - ConnectionKeepAlive = TimeSpan.FromSeconds(100); - ServerLog = log; - } - - /// <summary> - /// A log provider that all server related log entiries will be written to - /// </summary> - public readonly ILogProvider ServerLog { get; } + /// <summary> /// The absolute request entity body size limit in bytes /// </summary> public readonly int MaxUploadSize { get; init; } = 5 * 1000 * 1024; + /// <summary> /// The maximum size in bytes allowed for an MIME form-data content type upload /// </summary> /// <remarks>Set to 0 to disabled mulit-part/form-data uploads</remarks> - public readonly int MaxFormDataUploadSize { get; init; } = 40 * 1024; - /// <summary> - /// The maximum buffer size to use when parsing Multi-part/Form-data file uploads - /// </summary> - /// <remarks> - /// This value is used to create the buffer used to read data from the input stream - /// into memory for parsing. Form-data uploads must be parsed in memory because - /// the data is not delimited by a content length. - /// </remarks> - public readonly int FormDataBufferSize { get; init; } = 8192; + public readonly int MaxFormDataUploadSize { get; init; } = 40 * 1024; + /// <summary> /// The maximum response entity size in bytes for which the library will allow compresssing response data /// </summary> /// <remarks>Set this value to 0 to disable response compression</remarks> public readonly int CompressionLimit { get; init; } = 1000 * 1024; + /// <summary> /// The minimum size (in bytes) of respones data that will be compressed /// </summary> public readonly int CompressionMinimum { get; init; } = 4096; + /// <summary> /// The maximum amount of time to listen for data from a connected, but inactive transport connection /// before closing them /// </summary> - public readonly TimeSpan ConnectionKeepAlive { get; init; } + public readonly TimeSpan ConnectionKeepAlive { get; init; } = TimeSpan.FromSeconds(100); + /// <summary> /// The encoding to use when sending and receiving HTTP data /// </summary> public readonly Encoding HttpEncoding { get; init; } = Encoding.UTF8; + /// <summary> /// Sets the compression level for response entity streams of all supported types when /// compression is used. /// </summary> public readonly CompressionLevel CompressionLevel { get; init; } = CompressionLevel.Optimal; + /// <summary> /// Sets the default Http version for responses when the client version cannot be parsed from the request /// </summary> public readonly HttpVersion DefaultHttpVersion { get; init; } = HttpVersion.Http11; - /// <summary> - /// The buffer size used to read HTTP headers from the transport. - /// </summary> - /// <remarks> - /// Setting this value too low will result in header parsing failures - /// and 400 Bad Request responses. Setting it too high can result in - /// resource abuse or high memory usage. 8k is usually a good value. - /// </remarks> - public readonly int HeaderBufferSize { get; init; } = 8192; + /// <summary> /// The amount of time (in milliseconds) to wait for data on a connection that is in a receive /// state, aka active receive. /// </summary> public readonly int ActiveConnectionRecvTimeout { get; init; } = 5000; + /// <summary> /// The amount of time (in milliseconds) to wait for data to be send to the client before /// the connection is closed /// </summary> public readonly int SendTimeout { get; init; } = 5000; + /// <summary> /// The maximum number of request headers allowed per request /// </summary> public readonly int MaxRequestHeaderCount { get; init; } = 100; + /// <summary> /// The maximum number of open transport connections, before 503 errors /// will be returned and new connections closed. /// </summary> /// <remarks>Set to 0 to disable request processing. Causes perminant 503 results</remarks> - public readonly int MaxOpenConnections { get; init; } = int.MaxValue; - /// <summary> - /// The size (in bytes) of the http response header accumulator buffer. - /// </summary> - /// <remarks> - /// Http responses use an internal accumulator to buffer all response headers - /// before writing them to the transport in on write operation. If this value - /// is too low, the response will fail to write. If it is too high, it - /// may cause resource exhaustion or high memory usage. - /// </remarks> - public readonly int ResponseHeaderBufferSize { get; init; } = 16 * 1024; - /// <summary> - /// The size (in bytes) of the buffer to use to discard unread request entity bodies - /// </summary> - public readonly int DiscardBufferSize { get; init; } = 64 * 1024; - /// <summary> - /// The size of the buffer to use when writing response data to the transport - /// </summary> - /// <remarks> - /// This value is the size of the buffer used to copy data from the response - /// entity stream, to the transport stream. - /// </remarks> - public readonly int ResponseBufferSize { get; init; } = 32 * 1024; - /// <summary> - /// The size of the buffer used to accumulate chunked response data before writing to the transport - /// </summary> - public readonly int ChunkedResponseAccumulatorSize { get; init; } = 64 * 1024; + public readonly int MaxOpenConnections { get; init; } = int.MaxValue; + /// <summary> /// An <see cref="ILogProvider"/> for writing verbose request logs. Set to <c>null</c> /// to disable verbose request logging /// </summary> public readonly ILogProvider? RequestDebugLog { get; init; } = null; + + /// <summary> + /// The buffer configuration for the server + /// </summary> + public readonly HttpBufferConfig BufferConfig { get; init; } = new(); } }
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/IHttpEvent.cs b/lib/Net.Http/src/IHttpEvent.cs index ec1dbb5..8cd8f77 100644 --- a/lib/Net.Http/src/Core/IHttpEvent.cs +++ b/lib/Net.Http/src/IHttpEvent.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -38,6 +38,7 @@ namespace VNLib.Net.Http /// Current connection information. (Like "$_SERVER" superglobal in PHP) /// </summary> IConnectionInfo Server { get; } + /// <summary> /// The <see cref="HttpServer"/> that this connection originated from /// </summary> @@ -48,10 +49,12 @@ namespace VNLib.Net.Http /// </summary> /// <remarks>Keys are case-insensitive</remarks> IReadOnlyDictionary<string, string> QueryArgs { get; } + /// <summary> /// If the request body has form data or url encoded arguments they are stored in key value format /// </summary> IReadOnlyDictionary<string, string> RequestArgs { get; } + /// <summary> /// Contains all files upladed with current request /// </summary> diff --git a/lib/Net.Http/src/IHttpMemoryPool.cs b/lib/Net.Http/src/IHttpMemoryPool.cs new file mode 100644 index 0000000..f0a548e --- /dev/null +++ b/lib/Net.Http/src/IHttpMemoryPool.cs @@ -0,0 +1,52 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IHttpMemoryPool.cs +* +* IHttpMemoryPool.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.Buffers; + +using VNLib.Utils.Memory; + +namespace VNLib.Net.Http +{ + /// <summary> + /// Represents a single memory pool for the server that allocates buffers per http context. + /// on new connections and frees them when the connection is closed. + /// </summary> + public interface IHttpMemoryPool + { + /// <summary> + /// Allocates a buffer for a new http context connection attachment. + /// </summary> + /// <param name="bufferSize">The minium size of the buffer required</param> + /// <returns>A handle to the allocated buffer</returns> + IMemoryOwner<byte> AllocateBufferForContext(int bufferSize); + + /// <summary> + /// Allocates arbitrary form data related memory handles that are not tied to a specific http context. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="initialSize">The initial size of the buffer to allocate, which may be expanded as needed</param> + /// <returns>The allocated block of memory</returns> + MemoryHandle<T> AllocFormDataBuffer<T>(int initialSize) where T: unmanaged; + } +} |