From 546abea662263ef112c571c29706c47e875e09c4 Mon Sep 17 00:00:00 2001 From: vnugent Date: Sun, 10 Dec 2023 16:26:41 -0500 Subject: http overhaul, fix range, remove range auto-processing, fix some perf bottlenecks from profiling --- .../Core/Buffering/ContextLockedBufferManager.cs | 28 ++- .../src/Core/Buffering/HttpBufferElement.cs | 77 +++++-- lib/Net.Http/src/Core/Buffering/IHttpBuffer.cs | 21 +- .../src/Core/Buffering/ISplitHttpBuffer.cs | 2 +- .../src/Core/Buffering/SplitHttpBufferElement.cs | 21 +- lib/Net.Http/src/Core/ConnectionInfo.cs | 24 +- lib/Net.Http/src/Core/HttpContext.cs | 54 +++-- lib/Net.Http/src/Core/HttpEncodedSegment.cs | 71 +++++- lib/Net.Http/src/Core/HttpEvent.cs | 57 +++-- lib/Net.Http/src/Core/HttpServerBase.cs | 79 +++---- lib/Net.Http/src/Core/HttpServerProcessing.cs | 58 ++--- lib/Net.Http/src/Core/IHttpContextInformation.cs | 9 +- lib/Net.Http/src/Core/IHttpResponseBody.cs | 3 +- lib/Net.Http/src/Core/Request/HttpRequest.cs | 182 +++++++++------ .../src/Core/Request/HttpRequestExtensions.cs | 93 ++++---- lib/Net.Http/src/Core/Request/HttpRequestState.cs | 119 ++++++++++ .../src/Core/RequestParse/Http11ParseExtensions.cs | 169 +++++++++----- .../src/Core/Response/ChunkDataAccumulator.cs | 194 +++++++--------- lib/Net.Http/src/Core/Response/ChunkedStream.cs | 97 -------- lib/Net.Http/src/Core/Response/DirectStream.cs | 39 ---- .../src/Core/Response/HeaderDataAccumulator.cs | 55 ++--- .../src/Core/Response/HttpContextExtensions.cs | 50 +---- .../Core/Response/HttpContextResponseWriting.cs | 75 +++---- lib/Net.Http/src/Core/Response/HttpResponse.cs | 140 ++++++++---- .../src/Core/Response/IDirectResponsWriter.cs | 6 - lib/Net.Http/src/Core/Response/ResponseWriter.cs | 247 ++++++++++----------- .../src/Core/Response/ReusableResponseStream.cs | 4 +- lib/Net.Http/src/Core/ServerPreEncodedSegments.cs | 46 ---- lib/Net.Http/src/Core/TransportReader.cs | 170 ++++---------- lib/Net.Http/src/Helpers/HttpControlMask.cs | 44 ++++ lib/Net.Http/src/Helpers/HttpRange.cs | 44 ++++ lib/Net.Http/src/Helpers/HttpRangeType.cs | 52 +++++ lib/Net.Http/src/HttpBufferConfig.cs | 3 +- lib/Net.Http/src/HttpConfig.cs | 13 +- lib/Net.Http/src/IConnectionInfo.cs | 5 +- lib/Net.Http/src/IHttpEvent.cs | 10 +- .../src/SocketPipeLineWorker.cs | 5 + .../src/Endpoints/ResourceEndpointBase.cs | 24 +- lib/Plugins.Essentials/src/EventProcessor.cs | 87 +++++++- .../src/Extensions/ConnectionInfoExtensions.cs | 56 +++++ .../src/Extensions/EssentialHttpEventExtensions.cs | 21 +- lib/Plugins.Essentials/src/HttpEntity.cs | 7 +- 42 files changed, 1438 insertions(+), 1123 deletions(-) create mode 100644 lib/Net.Http/src/Core/Request/HttpRequestState.cs delete mode 100644 lib/Net.Http/src/Core/Response/ChunkedStream.cs delete mode 100644 lib/Net.Http/src/Core/Response/DirectStream.cs delete mode 100644 lib/Net.Http/src/Core/ServerPreEncodedSegments.cs create mode 100644 lib/Net.Http/src/Helpers/HttpControlMask.cs create mode 100644 lib/Net.Http/src/Helpers/HttpRange.cs create mode 100644 lib/Net.Http/src/Helpers/HttpRangeType.cs diff --git a/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs b/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs index ac2da25..827af56 100644 --- a/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs +++ b/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs @@ -47,13 +47,15 @@ namespace VNLib.Net.Http.Core.Buffering private readonly HeaderAccumulatorBuffer _requestHeaderBuffer; private readonly HeaderAccumulatorBuffer _responseHeaderBuffer; private readonly ChunkAccBuffer _chunkAccBuffer; + private readonly bool _chunkingEnabled; - public ContextLockedBufferManager(in HttpBufferConfig config) + public ContextLockedBufferManager(in HttpBufferConfig config, bool chunkingEnabled) { Config = config; + _chunkingEnabled = chunkingEnabled; //Compute total buffer size from server config - TotalBufferSize = ComputeTotalBufferSize(in config); + TotalBufferSize = ComputeTotalBufferSize(in config, chunkingEnabled); /* * Individual instances of the header accumulator buffer are required @@ -96,8 +98,11 @@ namespace VNLib.Net.Http.Core.Buffering //Shared response and form data buffer ResponseAndFormData = GetNextSegment(ref full, responseAndFormDataSize), - //Buffers cannot be shared - ChunkedResponseAccumulator = GetNextSegment(ref full, Config.ChunkedResponseAccumulatorSize) + /* + * The chunk accumulator buffer cannot be shared. It is also only + * stored if chunking is enabled. + */ + ChunkedResponseAccumulator = _chunkingEnabled ? GetNextSegment(ref full, Config.ChunkedResponseAccumulatorSize) : default }; /* @@ -205,12 +210,19 @@ namespace VNLib.Net.Http.Core.Buffering return SplitHttpBufferElement.GetfullSize(max); } - static int ComputeTotalBufferSize(in HttpBufferConfig config) + static int ComputeTotalBufferSize(in HttpBufferConfig config, bool chunkingEnabled) { - return config.ResponseBufferSize - + config.ChunkedResponseAccumulatorSize + int baseSize = config.ResponseBufferSize + ComputeResponseAndFormDataBuffer(in config) + GetMaxHeaderBufferSize(in config); //Header buffers are shared + + if (chunkingEnabled) + { + //Add chunking buffer + baseSize += config.ChunkedResponseAccumulatorSize; + } + + return baseSize; } static int ComputeResponseAndFormDataBuffer(in HttpBufferConfig config) @@ -220,7 +232,7 @@ namespace VNLib.Net.Http.Core.Buffering } - readonly record struct HttpBufferSegments + readonly struct HttpBufferSegments { public readonly Memory HeaderAccumulator { get; init; } public readonly Memory ChunkedResponseAccumulator { get; init; } diff --git a/lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs b/lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs index bffc1c5..0d26017 100644 --- a/lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs +++ b/lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs @@ -24,6 +24,7 @@ using System; using System.Buffers; +using System.Diagnostics; using System.Runtime.CompilerServices; using VNLib.Utils.Memory; @@ -41,44 +42,74 @@ namespace VNLib.Net.Http.Core.Buffering */ internal abstract class HttpBufferElement : IHttpBuffer { - private MemoryHandle Pinned; - private int _size; - protected Memory Buffer; + private HandleState _handle; - public virtual void FreeBuffer() + public void FreeBuffer() { - //Unpin and set defaults - Pinned.Dispose(); - Pinned = default; - Buffer = default; - _size = 0; + _handle.Unpin(); + _handle = default; } - public virtual void SetBuffer(Memory buffer) - { - //Set mem buffer - Buffer = buffer; - //Pin buffer and hold handle - Pinned = buffer.Pin(); - //Set size to length of buffer - _size = buffer.Length; - } + public void SetBuffer(Memory buffer) => _handle = new(buffer); + + /// + public int Size => _handle.Size; /// - public int Size => _size; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual Span GetBinSpan(int offset) => GetBinSpan(offset, Size - offset); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public virtual Span GetBinSpan() => MemoryUtil.GetSpan(ref Pinned, _size); + public virtual ref byte DangerousGetBinRef(int offset) + { + if (offset >= _handle.Size) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + //Get ref to pinned memory + ref byte start = ref _handle.GetRef(); + + //Add offset to ref + return ref Unsafe.Add(ref start, offset); + } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public virtual Memory GetMemory() => Buffer; + public virtual Memory GetMemory() => _handle.Memory; + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected virtual Span GetBinSpan(int maxSize) + public virtual Span GetBinSpan(int offset, int size) + => (offset + size) < _handle.Size ? _handle.GetSpan(offset, size) : throw new ArgumentOutOfRangeException(nameof(offset)); + + + private readonly struct HandleState { - return maxSize > _size ? throw new ArgumentOutOfRangeException(nameof(maxSize)) : MemoryUtil.GetSpan(ref Pinned, maxSize); + private readonly MemoryHandle _handle; + private readonly IntPtr _pointer; + + public readonly int Size; + public readonly Memory Memory; + + public HandleState(Memory mem) + { + Memory = mem; + Size = mem.Length; + _handle = mem.Pin(); + _pointer = MemoryUtil.GetIntptr(ref _handle); + } + + public readonly void Unpin() => _handle.Dispose(); + + public readonly Span GetSpan(int offset, int size) + { + Debug.Assert((offset + size) < Size, "Call to GetSpan failed because the offset/size was out of valid range"); + return MemoryUtil.GetSpan(IntPtr.Add(_pointer, offset), size); + } + + public readonly ref byte GetRef() => ref MemoryUtil.GetRef(_pointer); } } } diff --git a/lib/Net.Http/src/Core/Buffering/IHttpBuffer.cs b/lib/Net.Http/src/Core/Buffering/IHttpBuffer.cs index 07c7618..7a1156f 100644 --- a/lib/Net.Http/src/Core/Buffering/IHttpBuffer.cs +++ b/lib/Net.Http/src/Core/Buffering/IHttpBuffer.cs @@ -35,8 +35,27 @@ namespace VNLib.Net.Http.Core.Buffering /// /// Gets the internal buffer as a span of bytes as fast as possible /// + /// The number of bytes to offset the start of the segment /// The memory block as a span - Span GetBinSpan(); + Span GetBinSpan(int offset); + + /// + /// Gets the internal buffer as a span of bytes as fast as possible + /// with a specified offset and size + /// + /// The number of bytes to offset the start of the segment + /// The size of the desired segment + /// + Span GetBinSpan(int offset, int size); + + /// + /// Gets the internal buffer as a reference to a byte as fast as possible. + /// Dangerous because it's giving accessing a reference to the internal + /// memory buffer directly + /// + /// The number of bytes to offset the returned reference to + /// A reference to the first byte of the desired sequence + ref byte DangerousGetBinRef(int offset); /// /// Gets the internal buffer as a memory block as fast as possible diff --git a/lib/Net.Http/src/Core/Buffering/ISplitHttpBuffer.cs b/lib/Net.Http/src/Core/Buffering/ISplitHttpBuffer.cs index 2e0963d..6b6351f 100644 --- a/lib/Net.Http/src/Core/Buffering/ISplitHttpBuffer.cs +++ b/lib/Net.Http/src/Core/Buffering/ISplitHttpBuffer.cs @@ -32,7 +32,7 @@ namespace VNLib.Net.Http.Core.Buffering internal interface ISplitHttpBuffer : IHttpBuffer { /// - /// Gets the character segment of the internal buffer as a span of chars, which may be slower than + /// Gets the character segment of the internal buffer as a span of chars, which may be slower than /// but still considered a hot-path /// /// The character segment of the internal buffer diff --git a/lib/Net.Http/src/Core/Buffering/SplitHttpBufferElement.cs b/lib/Net.Http/src/Core/Buffering/SplitHttpBufferElement.cs index 103d723..b6d0d39 100644 --- a/lib/Net.Http/src/Core/Buffering/SplitHttpBufferElement.cs +++ b/lib/Net.Http/src/Core/Buffering/SplitHttpBufferElement.cs @@ -35,19 +35,13 @@ namespace VNLib.Net.Http.Core.Buffering /// public int BinSize { get; } - internal SplitHttpBufferElement(int binSize) - { - BinSize = binSize; - } + internal SplitHttpBufferElement(int binSize) => BinSize = binSize; /// public Span GetCharSpan() { - //Get full buffer span - Span _base = base.GetBinSpan(); - - //Upshift to end of bin buffer - _base = _base[BinSize..]; + //Get space available after binary buffer + Span _base = base.GetBinSpan(BinSize); //Return char span return MemoryMarshal.Cast(_base); @@ -59,8 +53,15 @@ namespace VNLib.Net.Http.Core.Buffering */ /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override Span GetBinSpan() => base.GetBinSpan(BinSize); + public override Span GetBinSpan(int offset) => base.GetBinSpan(offset, BinSize); + /* + * Override to trim the bin buffer to the actual size of the + * binary segment of the buffer + */ + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override Span GetBinSpan(int offset, int size) => base.GetBinSpan(offset, Math.Min(BinSize, size)); /// /// Gets the size total of the buffer required for binary data and char data diff --git a/lib/Net.Http/src/Core/ConnectionInfo.cs b/lib/Net.Http/src/Core/ConnectionInfo.cs index 325d53d..ff73cce 100644 --- a/lib/Net.Http/src/Core/ConnectionInfo.cs +++ b/lib/Net.Http/src/Core/ConnectionInfo.cs @@ -37,13 +37,13 @@ namespace VNLib.Net.Http private HttpContext Context; /// - public Uri RequestUri => Context.Request.Location; + public Uri RequestUri => Context.Request.State.Location; /// public string Path => RequestUri.LocalPath; /// - public string? UserAgent => Context.Request.UserAgent; + public string? UserAgent => Context.Request.State.UserAgent; /// public IHeaderCollection Headers { get; private set; } @@ -55,28 +55,28 @@ namespace VNLib.Net.Http public bool IsWebSocketRequest { get; } /// - public ContentType ContentType => Context.Request.ContentType; + public ContentType ContentType => Context.Request.State.ContentType; /// - public HttpMethod Method => Context.Request.Method; + public HttpMethod Method => Context.Request.State.Method; /// - public HttpVersion ProtocolVersion => Context.Request.HttpVersion; + public HttpVersion ProtocolVersion => Context.Request.State.HttpVersion; /// - public Uri? Origin => Context.Request.Origin; + public Uri? Origin => Context.Request.State.Origin; /// - public Uri? Referer => Context.Request.Referrer; + public Uri? Referer => Context.Request.State.Referrer; /// - public Tuple? Range => Context.Request.Range; + public HttpRange Range => Context.Request.State.Range; /// - public IPEndPoint LocalEndpoint => Context.Request.LocalEndPoint; + public IPEndPoint LocalEndpoint => Context.Request.State.LocalEndPoint; /// - public IPEndPoint RemoteEndpoint => Context.Request.RemoteEndPoint; + public IPEndPoint RemoteEndpoint => Context.Request.State.RemoteEndPoint; /// public Encoding Encoding => Context.ParentServer.Config.HttpEncoding; @@ -116,14 +116,14 @@ namespace VNLib.Net.Http internal ConnectionInfo(HttpContext ctx) { + //Update the context referrence + Context = ctx; //Create new header collection Headers = new VnHeaderCollection(ctx); //set co value CrossOrigin = ctx.Request.IsCrossOrigin(); //Set websocket status IsWebSocketRequest = ctx.Request.IsWebSocketRequest(); - //Update the context referrence - Context = ctx; } #nullable disable diff --git a/lib/Net.Http/src/Core/HttpContext.cs b/lib/Net.Http/src/Core/HttpContext.cs index feb3df5..3216a9b 100644 --- a/lib/Net.Http/src/Core/HttpContext.cs +++ b/lib/Net.Http/src/Core/HttpContext.cs @@ -31,17 +31,13 @@ using VNLib.Utils; using VNLib.Utils.Memory.Caching; using VNLib.Net.Http.Core.Buffering; using VNLib.Net.Http.Core.Compression; +using VNLib.Net.Http.Core.Response; namespace VNLib.Net.Http.Core { + internal sealed partial class HttpContext : IConnectionContext, IReusable, IHttpContextInformation { - /// - /// When set as a response flag, disables response compression for - /// the current request/response flow - /// - public const ulong COMPRESSION_DISABLED_MSK = 0x01UL; - /// /// The reusable http request container /// @@ -60,7 +56,7 @@ namespace VNLib.Net.Http.Core /// /// The response entity body container /// - public readonly IHttpResponseBody ResponseBody; + public readonly ResponseWriter ResponseBody; /// /// A collection of flags that can be used to control the way the context @@ -83,35 +79,35 @@ namespace VNLib.Net.Http.Core /// public IAlternateProtocol? AlternateProtocol { get; set; } - private readonly IResponseCompressor? _compressor; - private readonly ResponseWriter responseWriter; + private readonly ManagedHttpCompressor? _compressor; private ITransportContext? _ctx; public HttpContext(HttpServer server) { ParentServer = server; - //Init buffer manager - Buffers = new(server.Config.BufferConfig); - - //Create new request - Request = new (this); - - //create a new response object - Response = new (Buffers, this); - - //Init response writer - ResponseBody = responseWriter = new ResponseWriter(); + ContextFlags = new(0); /* * We can alloc a new compressor if the server supports compression. * If no compression is supported, the compressor will never be accessed */ - _compressor = server.SupportedCompressionMethods == CompressionMethod.None ? + _compressor = server.SupportedCompressionMethods == CompressionMethod.None ? null : new ManagedHttpCompressor(server.Config.CompressorManager!); - ContextFlags = new(0); + + //Init buffer manager, if compression is supported, we need to alloc a buffer for the compressor + Buffers = new(server.Config.BufferConfig, _compressor != null); + + //Create new request + Request = new (this, server.Config.MaxUploadsPerRequest); + + //create a new response object + Response = new (this, Buffers); + + //Init response writer + ResponseBody = new ResponseWriter(); } /// @@ -126,10 +122,13 @@ namespace VNLib.Net.Http.Core Encoding IHttpContextInformation.Encoding => ParentServer.Config.HttpEncoding; /// - HttpVersion IHttpContextInformation.CurrentVersion => Request.HttpVersion; + HttpVersion IHttpContextInformation.CurrentVersion => Request.State.HttpVersion; + + /// + public ref readonly HttpEncodedSegment CrlfSegment => ref ParentServer.CrlfBytes; /// - public ServerPreEncodedSegments EncodedSegments => ParentServer.PreEncodedSegments; + public ref readonly HttpEncodedSegment FinalChunkSegment => ref ParentServer.FinalChunkBytes; /// public Stream GetTransport() => _ctx!.ConnectionStream; @@ -157,11 +156,8 @@ namespace VNLib.Net.Http.Core ContextFlags.ClearAll(); //Lifecycle on new request - Request.OnNewRequest(); - Response.OnNewRequest(); - - //Initialize the request Request.Initialize(_ctx!, ParentServer.Config.DefaultHttpVersion); + Response.OnNewRequest(); } /// @@ -175,7 +171,7 @@ namespace VNLib.Net.Http.Core { Request.OnComplete(); Response.OnComplete(); - responseWriter.OnComplete(); + ResponseBody.OnComplete(); //Free compressor when a message flow is complete _compressor?.Free(); diff --git a/lib/Net.Http/src/Core/HttpEncodedSegment.cs b/lib/Net.Http/src/Core/HttpEncodedSegment.cs index c036a1b..1103f83 100644 --- a/lib/Net.Http/src/Core/HttpEncodedSegment.cs +++ b/lib/Net.Http/src/Core/HttpEncodedSegment.cs @@ -23,6 +23,13 @@ */ using System; +using System.Text; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +using VNLib.Utils.Memory; +using VNLib.Net.Http.Core.Buffering; namespace VNLib.Net.Http.Core { @@ -32,16 +39,70 @@ namespace VNLib.Net.Http.Core /// The buffer containing the segment data /// The offset in the buffer to begin the segment at /// The length of the segment - internal readonly record struct HttpEncodedSegment(byte[] Buffer, int Offset, int Length) + internal readonly record struct HttpEncodedSegment(byte[] Buffer, uint Offset, uint Length) { /// - /// Span representation of the pre-encoded segment + /// Validates the bounds of the array so calls to + /// won't cause a buffer over/under run + /// + public readonly void Validate() => MemoryUtil.CheckBounds(Buffer, Offset, Length); + + /// + /// Performs a dangerous reference based copy-to (aka memmove) + /// + /// The output buffer to write the encoded segment to + internal readonly int DangerousCopyTo(Span output) + { + Debug.Assert(output.Length >= Length, "Output span was empty and could not be written to"); + + //Get reference of output buffer span + return DangerousCopyTo(ref MemoryMarshal.GetReference(output)); + } + + /// + /// Performs a dangerous reference based copy-to (aka memmove) + /// to the supplied at the supplied offset. + /// This operation performs bounds checks /// - public Span Span => Buffer.AsSpan(Offset, Length); + /// The to copy data to + /// The byte offset to the first byte of the desired segment + /// The number of bytes written to the segment + /// + internal readonly int DangerousCopyTo(IHttpBuffer buffer, int offset) + { + //Ensure enough space is available + if(offset + Length <= buffer.Size) + { + ref byte dst = ref buffer.DangerousGetBinRef(offset); + return DangerousCopyTo(ref dst); + } + + throw new ArgumentOutOfRangeException(nameof(offset), "Buffer is too small to hold the encoded segment"); + } + + private readonly int DangerousCopyTo(ref byte output) + { + Debug.Assert(!Unsafe.IsNullRef(ref output), "Output span was empty and could not be written to"); + + //Get references of output buffer and array buffer + ref byte src = ref MemoryMarshal.GetArrayDataReference(Buffer); + + //Call memmove with the buffer offset and desired length + MemoryUtil.Memmove(ref src, Offset, ref output, 0, Length); + return (int)Length; + } /// - /// Memory representation of the pre-encoded segment + /// Allocates a new buffer from the supplied string + /// using the supplied encoding /// - public Memory Memory => Buffer.AsMemory(Offset, Length); + /// The string data to encode + /// The encoder used to convert the character data to bytes + /// The initalized structure + public static HttpEncodedSegment FromString(string data, Encoding enc) + { + byte[] encoded = enc.GetBytes(data); + return new HttpEncodedSegment(encoded, 0, (uint)encoded.Length); + } } } \ No newline at end of file diff --git a/lib/Net.Http/src/Core/HttpEvent.cs b/lib/Net.Http/src/Core/HttpEvent.cs index 7d7c1e7..ee6a380 100644 --- a/lib/Net.Http/src/Core/HttpEvent.cs +++ b/lib/Net.Http/src/Core/HttpEvent.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -29,19 +29,22 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using VNLib.Net.Http.Core; +using VNLib.Net.Http.Core.Response; namespace VNLib.Net.Http -{ +{ internal sealed class HttpEvent : MarshalByRefObject, IHttpEvent { private HttpContext Context; private ConnectionInfo _ci; + private FileUpload[] _uploads; internal HttpEvent(HttpContext ctx) { Context = ctx; _ci = new ConnectionInfo(ctx); - } + _uploads = ctx.Request.CopyUploads(); + } /// IConnectionInfo IHttpEvent.Server => _ci; @@ -50,14 +53,16 @@ namespace VNLib.Net.Http HttpServer IHttpEvent.OriginServer => Context.ParentServer; /// - IReadOnlyDictionary IHttpEvent.QueryArgs => Context.Request.RequestBody.QueryArgs; + IReadOnlyDictionary IHttpEvent.QueryArgs => Context.Request.QueryArgs; + /// - IReadOnlyDictionary IHttpEvent.RequestArgs => Context.Request.RequestBody.RequestArgs; + IReadOnlyDictionary IHttpEvent.RequestArgs => Context.Request.RequestArgs; + /// - IReadOnlyList IHttpEvent.Files => Context.Request.RequestBody.Uploads; + IReadOnlyList IHttpEvent.Files => _uploads; /// - void IHttpEvent.DisableCompression() => Context.ContextFlags.Set(HttpContext.COMPRESSION_DISABLED_MSK); + void IHttpEvent.SetControlFlag(ulong mask) => Context.ContextFlags.Set(mask); /// void IHttpEvent.DangerousChangeProtocol(IAlternateProtocol protocolHandler) @@ -79,16 +84,23 @@ namespace VNLib.Net.Http void IHttpEvent.CloseResponse(HttpStatusCode code) => Context.Respond(code); /// - void IHttpEvent.CloseResponse(HttpStatusCode code, ContentType type, Stream stream) + void IHttpEvent.CloseResponse(HttpStatusCode code, ContentType type, Stream stream, long length) { + ArgumentNullException.ThrowIfNull(stream, nameof(stream)); + + if(length < 0) + { + throw new ArgumentException("Length must be greater than or equal to 0", nameof(length)); + } + //Check if the stream is valid. We will need to read the stream, and we will also need to get the length property - if (!stream.CanSeek || !stream.CanRead) + if (!stream.CanRead) { throw new IOException("The stream.Length property must be available and the stream must be readable"); } //If stream is empty, ignore it, the server will default to 0 content length and avoid overhead - if (stream.Length == 0) + if (length == 0) { return; } @@ -97,18 +109,24 @@ namespace VNLib.Net.Http Context.Response.SetStatusCode(code); //Finally store the stream input - if(!(Context.ResponseBody as ResponseWriter)!.TrySetResponseBody(stream)) + if(!Context.ResponseBody.TrySetResponseBody(stream, length)) { throw new InvalidOperationException("A response body has already been set"); } - //Set content type header after body - Context.Response.Headers[HttpResponseHeader.ContentType] = HttpHelpers.GetContentTypeString(type); + //User may want to set the content type header themselves + if (type != ContentType.NonSupported) + { + //Set content type header after body + Context.Response.Headers.Set(HttpResponseHeader.ContentType, HttpHelpers.GetContentTypeString(type)); + } } /// void IHttpEvent.CloseResponse(HttpStatusCode code, ContentType type, IMemoryResponseReader entity) { + ArgumentNullException.ThrowIfNull(entity, nameof(entity)); + //If stream is empty, ignore it, the server will default to 0 content length and avoid overhead if (entity.Remaining == 0) { @@ -118,14 +136,18 @@ namespace VNLib.Net.Http //Set status code Context.Response.SetStatusCode(code); - //Finally store the stream input - if (!(Context.ResponseBody as ResponseWriter)!.TrySetResponseBody(entity)) + //Store the memory reader input + if (!Context.ResponseBody.TrySetResponseBody(entity)) { throw new InvalidOperationException("A response body has already been set"); } - //Set content type header after body - Context.Response.Headers[HttpResponseHeader.ContentType] = HttpHelpers.GetContentTypeString(type); + //User may want to set the content type header themselves + if (type != ContentType.NonSupported) + { + //Set content type header after body + Context.Response.Headers.Set(HttpResponseHeader.ContentType, HttpHelpers.GetContentTypeString(type)); + } } #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. @@ -135,6 +157,7 @@ namespace VNLib.Net.Http Context = null; _ci.Clear(); _ci = null; + _uploads = null; } #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } diff --git a/lib/Net.Http/src/Core/HttpServerBase.cs b/lib/Net.Http/src/Core/HttpServerBase.cs index f9e0418..ec42e5b 100644 --- a/lib/Net.Http/src/Core/HttpServerBase.cs +++ b/lib/Net.Http/src/Core/HttpServerBase.cs @@ -36,7 +36,6 @@ */ using System; -using System.Text; using System.Linq; using System.Threading; using System.Net.Sockets; @@ -86,10 +85,12 @@ namespace VNLib.Net.Http /// The cached HTTP1/1 keepalive timeout header value /// private readonly string KeepAliveTimeoutHeaderValue; + /// /// Reusable store for obtaining /// private readonly ObjectRental ContextStore; + /// /// The cached header-line termination value /// @@ -106,15 +107,21 @@ namespace VNLib.Net.Http /// public bool Running { get; private set; } - /// - /// The for the current server - /// - internal readonly ServerPreEncodedSegments PreEncodedSegments; /// /// Cached supported compression methods /// internal readonly CompressionMethod SupportedCompressionMethods; + /// + /// Pre-encoded CRLF bytes + /// + internal readonly HttpEncodedSegment CrlfBytes; + + /// + /// Pre-encoded HTTP chunking final chunk segment + /// + internal readonly HttpEncodedSegment FinalChunkBytes; + private CancellationTokenSource? StopToken; /// @@ -139,10 +146,6 @@ namespace VNLib.Net.Http ContextStore = ObjectRental.CreateReusable(() => new HttpContext(this)); //Setup config copy with the internal http pool Transport = transport; - //Create the pre-encoded segments - PreEncodedSegments = GetSegments(config.HttpEncoding); - //Store a ref to the crlf memory segment - HeaderLineTermination = PreEncodedSegments.CrlfBytes.Memory; //Cache supported compression methods, or none if compressor is null SupportedCompressionMethods = config.CompressorManager == null ? CompressionMethod.None : @@ -150,6 +153,13 @@ namespace VNLib.Net.Http //Cache wildcard root _wildcardRoot = ServerRoots.GetValueOrDefault(WILDCARD_KEY); + + //Init pre-encded segments + CrlfBytes = HttpEncodedSegment.FromString(HttpHelpers.CRLF, config.HttpEncoding); + FinalChunkBytes = HttpEncodedSegment.FromString("0\r\n\r\n", config.HttpEncoding); + + //Store a ref to the crlf memory segment + HeaderLineTermination = CrlfBytes.Buffer.AsMemory(); } private static void ValidateConfig(in HttpConfig conf) @@ -163,10 +173,21 @@ namespace VNLib.Net.Http throw new ArgumentException("ActiveConnectionRecvTimeout cannot be less than -1", nameof(conf)); } - //Chunked data accumulator must be at least 64 bytes (arbinrary value) - if (conf.BufferConfig.ChunkedResponseAccumulatorSize < 64 || conf.BufferConfig.ChunkedResponseAccumulatorSize == int.MaxValue) + //We only need to verify the chunk buffer size if compression is enabled, otherwise it will never be used + if (conf.CompressorManager != null) { - throw new ArgumentException("ChunkedResponseAccumulatorSize cannot be less than 64 bytes", nameof(conf)); + //Chunked data accumulator must be at least 64 bytes (arbitrary value) + if (conf.BufferConfig.ChunkedResponseAccumulatorSize < 64 || conf.BufferConfig.ChunkedResponseAccumulatorSize == int.MaxValue) + { + throw new ArgumentException("ChunkedResponseAccumulatorSize cannot be less than 64 bytes", nameof(conf)); + } + } + else + { + if(conf.BufferConfig.ChunkedResponseAccumulatorSize < 0) + { + throw new ArgumentException("ChunkedResponseAccumulatorSize can never be less than 0", nameof(conf)); + } } if (conf.CompressionLimit < 0) @@ -230,40 +251,6 @@ namespace VNLib.Net.Http } } - private static ServerPreEncodedSegments GetSegments(Encoding encoding) - { - int offset = 0, length; - - //Alloc buffer to store segments in - byte[] buffer = new byte[16]; - - Span span = buffer; - - //Get crlf bytes - length = encoding.GetBytes(HttpHelpers.CRLF, span); - - //Build crlf segment - HttpEncodedSegment crlf = new(buffer, offset, length); - - offset += length; - span = span[offset..]; - - //Get final chunk bytes - length = encoding.GetBytes("0\r\n\r\n", span); - - //Build final chunk segment - HttpEncodedSegment finalChunk = new(buffer, offset, length); - - offset += length; - - //Build the segments - return new ServerPreEncodedSegments(buffer) - { - FinalChunkTermination = finalChunk, - CrlfBytes = crlf - }; - } - /// /// Begins listening for connections on configured interfaces for configured hostnames. /// diff --git a/lib/Net.Http/src/Core/HttpServerProcessing.cs b/lib/Net.Http/src/Core/HttpServerProcessing.cs index 7d01e23..61adbbc 100644 --- a/lib/Net.Http/src/Core/HttpServerProcessing.cs +++ b/lib/Net.Http/src/Core/HttpServerProcessing.cs @@ -36,6 +36,7 @@ using VNLib.Utils.Memory; using VNLib.Utils.Logging; using VNLib.Net.Http.Core; using VNLib.Net.Http.Core.Buffering; +using VNLib.Net.Http.Core.Response; namespace VNLib.Net.Http { @@ -55,8 +56,10 @@ namespace VNLib.Net.Http try { + Stream stream = transportContext.ConnectionStream; + //Set write timeout - transportContext.ConnectionStream.WriteTimeout = _config.SendTimeout; + stream.WriteTimeout = _config.SendTimeout; //Init stream context.InitializeContext(transportContext); @@ -65,7 +68,7 @@ namespace VNLib.Net.Http do { //Set rx timeout low for initial reading - transportContext.ConnectionStream.ReadTimeout = _config.ActiveConnectionRecvTimeout; + stream.ReadTimeout = _config.ActiveConnectionRecvTimeout; //Process the request bool keepAlive = await ProcessHttpEventAsync(context); @@ -77,10 +80,10 @@ namespace VNLib.Net.Http } //Reset inactive keeaplive timeout, when expired the following read will throw a cancealltion exception - transportContext.ConnectionStream.ReadTimeout = (int)_config.ConnectionKeepAlive.TotalMilliseconds; + stream.ReadTimeout = (int)_config.ConnectionKeepAlive.TotalMilliseconds; //"Peek" or wait for more data to begin another request (may throw timeout exception when timmed out) - await transportContext.ConnectionStream.ReadAsync(Memory.Empty, StopToken!.Token); + await stream.ReadAsync(Memory.Empty, StopToken!.Token); } while (true); @@ -95,11 +98,11 @@ namespace VNLib.Net.Http context = null; //Remove transport timeouts - transportContext.ConnectionStream.ReadTimeout = Timeout.Infinite; - transportContext.ConnectionStream.WriteTimeout = Timeout.Infinite; + stream.ReadTimeout = Timeout.Infinite; + stream.WriteTimeout = Timeout.Infinite; //Listen on the alternate protocol - await ap.RunAsync(transportContext.ConnectionStream, StopToken!.Token).ConfigureAwait(false); + await ap.RunAsync(stream, StopToken!.Token).ConfigureAwait(false); } } //Catch wrapped socket exceptions @@ -249,7 +252,9 @@ namespace VNLib.Net.Http //Init parser TransportReader reader = new (ctx.GetTransport(), parseBuffer, _config.HttpEncoding, HeaderLineTermination); - + + HttpStatusCode code; + try { //Get the char span @@ -257,24 +262,19 @@ namespace VNLib.Net.Http Http11ParseExtensions.Http1ParseState parseState = new(); - //Parse the request line - HttpStatusCode code = ctx.Request.Http1ParseRequestLine(ref parseState, ref reader, lineBuf, secInfo.HasValue); - - if (code > 0) + if ((code = ctx.Request.Http1ParseRequestLine(ref parseState, ref reader, lineBuf, secInfo.HasValue)) > 0) { return code; } //Parse the headers - code = ctx.Request.Http1ParseHeaders(ref parseState, ref reader, Config, lineBuf); - if (code > 0) + if ((code = ctx.Request.Http1ParseHeaders(ref parseState, ref reader, Config, lineBuf)) > 0) { return code; } //Prepare entity body for request - code = ctx.Request.Http1PrepareEntityBody(ref parseState, ref reader, Config); - if (code > 0) + if ((code = ctx.Request.Http1PrepareEntityBody(ref parseState, ref reader, Config)) > 0) { return code; } @@ -306,18 +306,18 @@ namespace VNLib.Net.Http * connection if parsing the request fails */ //Close the connection when we exit - context.Response.Headers[HttpResponseHeader.Connection] = "closed"; + context.Response.Headers.Set(HttpResponseHeader.Connection, "closed"); //Return status code, if the the expect header was set, return expectation failed, otherwise return the result status code - context.Respond(context.Request.Expect ? HttpStatusCode.ExpectationFailed : status); + context.Respond(context.Request.State.Expect ? HttpStatusCode.ExpectationFailed : status); //exit and close connection (default result will close the context) return false; } //Make sure the server supports the http version - if ((context.Request.HttpVersion & SupportedVersions) == 0) + if ((context.Request.State.HttpVersion & SupportedVersions) == 0) { //Close the connection when we exit - context.Response.Headers[HttpResponseHeader.Connection] = "closed"; + context.Response.Headers.Set(HttpResponseHeader.Connection, "closed"); context.Respond(HttpStatusCode.HttpVersionNotSupported); return false; } @@ -326,13 +326,13 @@ namespace VNLib.Net.Http if (OpenConnectionCount > _config.MaxOpenConnections) { //Close the connection and return 503 - context.Response.Headers[HttpResponseHeader.Connection] = "closed"; + context.Response.Headers.Set(HttpResponseHeader.Connection, "closed"); context.Respond(HttpStatusCode.ServiceUnavailable); return false; } //Store keepalive value from request, and check if keepalives are enabled by the configuration - bool keepalive = context.Request.KeepAlive & _config.ConnectionKeepAlive > TimeSpan.Zero; + bool keepalive = context.Request.State.KeepAlive & _config.ConnectionKeepAlive > TimeSpan.Zero; //Set connection header (only for http1.1) @@ -342,19 +342,19 @@ namespace VNLib.Net.Http * Request parsing only sets the keepalive flag if the connection is http1.1 * so we can verify this in an assert */ - Debug.Assert(context.Request.HttpVersion == HttpVersion.Http11, "Request parsing allowed keepalive for a non http1.1 connection, this is a bug"); + Debug.Assert(context.Request.State.HttpVersion == HttpVersion.Http11, "Request parsing allowed keepalive for a non http1.1 connection, this is a bug"); - context.Response.Headers[HttpResponseHeader.Connection] = "keep-alive"; - context.Response.Headers[HttpResponseHeader.KeepAlive] = KeepAliveTimeoutHeaderValue; + context.Response.Headers.Set(HttpResponseHeader.Connection, "keep-alive"); + context.Response.Headers.Set(HttpResponseHeader.KeepAlive, KeepAliveTimeoutHeaderValue); } else { //Set connection closed - context.Response.Headers[HttpResponseHeader.Connection] = "closed"; + context.Response.Headers.Set(HttpResponseHeader.Connection, "closed"); } - //Get the server root for the specified location - IWebRoot? root = ServerRoots!.GetValueOrDefault(context.Request.Location.DnsSafeHost, _wildcardRoot); + //Get the server root for the specified location or fallback to a wildcard host if one is selected + IWebRoot? root = ServerRoots!.GetValueOrDefault(context.Request.State.Location.DnsSafeHost, _wildcardRoot); if (root == null) { @@ -364,7 +364,7 @@ namespace VNLib.Net.Http } //Check the expect header and return an early status code - if (context.Request.Expect) + if (context.Request.State.Expect) { //send a 100 status code await context.Response.SendEarly100ContinueAsync(); diff --git a/lib/Net.Http/src/Core/IHttpContextInformation.cs b/lib/Net.Http/src/Core/IHttpContextInformation.cs index fbe079f..14067f5 100644 --- a/lib/Net.Http/src/Core/IHttpContextInformation.cs +++ b/lib/Net.Http/src/Core/IHttpContextInformation.cs @@ -31,9 +31,14 @@ namespace VNLib.Net.Http.Core internal interface IHttpContextInformation { /// - /// Gets pre-encoded binary segments for the current server's encoding + /// Gets a reference to the containing the CRLF sequence /// - ServerPreEncodedSegments EncodedSegments { get; } + ref readonly HttpEncodedSegment CrlfSegment { get; } + + /// + /// Gets a reference to the containing the final chunk sequence + /// + ref readonly HttpEncodedSegment FinalChunkSegment { get; } /// /// The current connection's encoding diff --git a/lib/Net.Http/src/Core/IHttpResponseBody.cs b/lib/Net.Http/src/Core/IHttpResponseBody.cs index 696e9da..facf8b0 100644 --- a/lib/Net.Http/src/Core/IHttpResponseBody.cs +++ b/lib/Net.Http/src/Core/IHttpResponseBody.cs @@ -61,9 +61,8 @@ namespace VNLib.Net.Http.Core /// /// The response stream to write data to /// An optional buffer used to buffer responses - /// The maximum length of the response data to write /// A task that resolves when the response is completed - Task WriteEntityAsync(IDirectResponsWriter dest, long count, Memory buffer); + Task WriteEntityAsync(IDirectResponsWriter dest, Memory buffer); /// /// Writes internal response entity data to the destination stream diff --git a/lib/Net.Http/src/Core/Request/HttpRequest.cs b/lib/Net.Http/src/Core/Request/HttpRequest.cs index cf21b19..43b3e5f 100644 --- a/lib/Net.Http/src/Core/Request/HttpRequest.cs +++ b/lib/Net.Http/src/Core/Request/HttpRequest.cs @@ -23,7 +23,6 @@ */ using System; -using System.Net; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -40,54 +39,56 @@ namespace VNLib.Net.Http.Core #endif { public readonly VnWebHeaderCollection Headers; - public readonly Dictionary Cookies; public readonly List Accept; public readonly List AcceptLanguage; - public readonly HttpRequestBody RequestBody; - - public HttpVersion HttpVersion { get; set; } - public HttpMethod Method { get; set; } - public string? UserAgent { get; set; } - public string? Boundry { get; set; } - public ContentType ContentType { get; set; } - public string? Charset { get; set; } - public Uri Location { get; set; } - public Uri? Origin { get; set; } - public Uri? Referrer { get; set; } - internal bool KeepAlive { get; set; } - public IPEndPoint RemoteEndPoint { get; set; } - public IPEndPoint LocalEndPoint { get; set; } - public Tuple? Range { get; set; } + public readonly Dictionary Cookies; + public readonly Dictionary RequestArgs; + public readonly Dictionary QueryArgs; /// - /// A value indicating whether the connection contained a request entity body. + /// A transport stream wrapper that is positioned for reading + /// the entity body from the input stream /// - public bool HasEntityBody { get; set; } + public readonly HttpInputStream InputStream; + + /* + * Evil mutable structure that stores the http request state. + * + * Readonly ref allows for immutable accessors, but + * explicit initialization function for a mutable ref + * that can be stored in local state to ensure proper state + * initalization. + * + * Reason - easy and mistake free object reuse with safe + * null/default values and easy reset. + */ + private HttpRequestState _state; + private readonly FileUpload[] _uploads; /// - /// A transport stream wrapper that is positioned for reading - /// the entity body from the input stream + /// Gets a mutable structure ref only used to initalize the request + /// state. /// - public HttpInputStream InputStream { get; } + /// A mutable reference to the state structure for initalization purposes + internal ref HttpRequestState GetMutableStateForInit() => ref _state; /// - /// A value indicating if the client's request had an Expect-100-Continue header + /// A readonly reference to the internal request state once initialized /// - public bool Expect { get; set; } + internal ref readonly HttpRequestState State => ref _state; -#nullable disable - public HttpRequest(IHttpContextInformation contextInfo) + public HttpRequest(IHttpContextInformation contextInfo, ushort maxUploads) { //Create new collection for headers + _uploads = new FileUpload[maxUploads]; Headers = new(); - //Create new collection for request cookies Cookies = new(5, StringComparer.OrdinalIgnoreCase); - //New list for accept - Accept = new(10); - AcceptLanguage = new(10); + RequestArgs = new(StringComparer.OrdinalIgnoreCase); + QueryArgs = new(StringComparer.OrdinalIgnoreCase); + Accept = new(8); + AcceptLanguage = new(8); //New reusable input stream InputStream = new(contextInfo); - RequestBody = new(); } void IHttpLifeCycle.OnPrepare() @@ -98,44 +99,87 @@ namespace VNLib.Net.Http.Core void IHttpLifeCycle.OnNewConnection() { } + + void IHttpLifeCycle.OnNewRequest() + { } + /// + /// Initializes the for an incomming connection + /// + /// The to attach the request to + /// The default http version [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void OnNewRequest() + public void Initialize(ITransportContext ctx, HttpVersion defaultHttpVersion) { - //Set to defaults - ContentType = ContentType.NonSupported; - Method = HttpMethod.None; - HttpVersion = HttpVersion.None; + _state.LocalEndPoint = ctx.LocalEndPoint; + _state.RemoteEndPoint = ctx.RemoteEndpoint; + //Set to default http version so the response can be configured properly + _state.HttpVersion = defaultHttpVersion; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void OnComplete() { + FreeUploadBuffers(); + + //Clear request state (this is why a struct is used) + _state = default; + //release the input stream InputStream.OnComplete(); - RequestBody.OnComplete(); + //Clear args + RequestArgs.Clear(); + QueryArgs.Clear(); //Make sure headers, cookies, and accept are cleared for reuse Headers.Clear(); Cookies.Clear(); Accept.Clear(); AcceptLanguage.Clear(); - //Clear request flags - this.Expect = false; - this.KeepAlive = false; - this.HasEntityBody = false; - //We need to clean up object refs - this.Boundry = default; - this.Charset = default; - this.LocalEndPoint = default; - this.Location = default; - this.Origin = default; - this.Referrer = default; - this.RemoteEndPoint = default; - this.UserAgent = default; - this.Range = default; - //We are all set to reuse the instance } + private void FreeUploadBuffers() + { + //Dispose all initialized files + for (int i = 0; i < _uploads.Length; i++) + { + _uploads[i].Free(); + _uploads[i] = default; + } + } + + public void AddFileUpload(in FileUpload upload) + { + //See if there is room for another upload + if (_state.UploadCount < _uploads.Length) + { + //Add file to upload array and increment upload count + _uploads[_state.UploadCount++] = upload; + } + } + + /// + /// Checks if another upload can be added to the request + /// + /// A value indicating if another file upload can be added to the array + public bool CanAddUpload() => _state.UploadCount < _uploads.Length; + + /// + /// Creates a new array and copies the uploads to it. + /// + /// The array clone of the file uploads + public FileUpload[] CopyUploads() + { + if (_state.UploadCount == 0) + { + return Array.Empty(); + } + //Create new array to hold uploads + FileUpload[] uploads = new FileUpload[_state.UploadCount]; + //Copy uploads to new array + Array.Copy(_uploads, uploads, _state.UploadCount); + //Return new array + return uploads; + } #if DEBUG @@ -154,11 +198,11 @@ namespace VNLib.Net.Http.Core public void Compile(ref ForwardOnlyWriter writer) { //Request line - writer.Append(Method.ToString()); + writer.Append(_state.Method.ToString()); writer.Append(" "); - writer.Append(Location?.PathAndQuery); + writer.Append(_state.Location?.PathAndQuery); writer.Append(" HTTP/"); - switch (HttpVersion) + switch (_state.HttpVersion) { case HttpVersion.None: writer.Append("Unsuppored Http version"); @@ -181,7 +225,7 @@ namespace VNLib.Net.Http.Core //write host writer.Append("Host: "); - writer.Append(Location?.Authority); + writer.Append(_state.Location?.Authority); writer.Append("\r\n"); //Write headers @@ -226,51 +270,51 @@ namespace VNLib.Net.Http.Core writer.Append("\r\n"); } //Write user agent - if (UserAgent != null) + if (_state.UserAgent != null) { writer.Append("User-Agent: "); - writer.Append(UserAgent); + writer.Append(_state.UserAgent); writer.Append("\r\n"); } //Write content type - if (ContentType != ContentType.NonSupported) + if (_state.ContentType != ContentType.NonSupported) { writer.Append("Content-Type: "); - writer.Append(HttpHelpers.GetContentTypeString(ContentType)); + writer.Append(HttpHelpers.GetContentTypeString(_state.ContentType)); writer.Append("\r\n"); } //Write content length - if (ContentType != ContentType.NonSupported) + if (_state.ContentType != ContentType.NonSupported) { writer.Append("Content-Length: "); writer.Append(InputStream.Length); writer.Append("\r\n"); } - if (KeepAlive) + if (_state.KeepAlive) { writer.Append("Connection: keep-alive\r\n"); } - if (Expect) + if (_state.Expect) { writer.Append("Expect: 100-continue\r\n"); } - if(Origin != null) + if(_state.Origin != null) { writer.Append("Origin: "); - writer.Append(Origin.ToString()); + writer.Append(_state.Origin.ToString()); writer.Append("\r\n"); } - if (Referrer != null) + if (_state.Referrer != null) { writer.Append("Referrer: "); - writer.Append(Referrer.ToString()); + writer.Append(_state.Referrer.ToString()); writer.Append("\r\n"); } writer.Append("from "); - writer.Append(RemoteEndPoint.ToString()); + writer.Append(_state.RemoteEndPoint.ToString()); writer.Append("\r\n"); writer.Append("Received on "); - writer.Append(LocalEndPoint.ToString()); + writer.Append(_state.LocalEndPoint.ToString()); //Write end of headers writer.Append("\r\n"); } diff --git a/lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs b/lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs index 8fef8dd..69bd2af 100644 --- a/lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs +++ b/lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs @@ -26,8 +26,8 @@ using System; using System.IO; using System.Net; using System.Text; +using System.Diagnostics; using System.Threading.Tasks; -using System.Security.Authentication; using System.Runtime.CompilerServices; using VNLib.Utils.IO; @@ -91,9 +91,9 @@ namespace VNLib.Net.Http.Core [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsCrossOrigin(this HttpRequest Request) { - return Request.Origin != null - && (!Request.Origin.Authority.Equals(Request.Location.Authority, StringComparison.Ordinal) - || !Request.Origin.Scheme.Equals(Request.Location.Scheme, StringComparison.Ordinal)); + return Request.State.Origin != null + && (!Request.State.Origin.Authority.Equals(Request.State.Location.Authority, StringComparison.Ordinal) + || !Request.State.Origin.Scheme.Equals(Request.State.Location.Scheme, StringComparison.Ordinal)); } /// @@ -116,22 +116,6 @@ namespace VNLib.Net.Http.Core return false; } - /// - /// Initializes the for an incomming connection - /// - /// - /// The to attach the request to - /// The default http version - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Initialize(this HttpRequest server, ITransportContext ctx, HttpVersion defaultHttpVersion) - { - server.LocalEndPoint = ctx.LocalEndPoint; - server.RemoteEndPoint = ctx.RemoteEndpoint; - //Set to default http version so the response can be configured properly - server.HttpVersion = defaultHttpVersion; - } - - /// /// Initializes the for the current request /// @@ -145,7 +129,7 @@ namespace VNLib.Net.Http.Core ParseQueryArgs(context.Request); //Decode requests from body - return !context.Request.HasEntityBody ? ValueTask.CompletedTask : ParseInputStream(context); + return !context.Request.State.HasEntityBody ? ValueTask.CompletedTask : ParseInputStream(context); } private static async ValueTask ParseInputStream(HttpContext context) @@ -163,7 +147,9 @@ namespace VNLib.Net.Http.Core //Get the form data buffer (should be cost free) Memory formBuffer = context.Buffers.GetFormDataBuffer(); - switch (request.ContentType) + Debug.Assert(!formBuffer.IsEmpty, "GetFormDataBuffer() returned an empty memory buffer"); + + switch (request.State.ContentType) { //CT not supported, dont read it case ContentType.NonSupported: @@ -185,7 +171,7 @@ namespace VNLib.Net.Http.Core case ContentType.MultiPart: { //Make sure we have a boundry specified - if (string.IsNullOrWhiteSpace(request.Boundry)) + if (string.IsNullOrWhiteSpace(request.State.Boundry)) { break; } @@ -198,14 +184,14 @@ namespace VNLib.Net.Http.Core //Split the body as a span at the boundries ((ReadOnlySpan)formBody.AsSpan(0, chars)) - .Split($"--{request.Boundry}", StringSplitOptions.RemoveEmptyEntries, FormDataBodySplitCb, context); + .Split($"--{request.State.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)); + //add upload (if it fails thats fine, no memory to clean up) + request.AddFileUpload(new(request.InputStream, false, request.State.ContentType, null)); break; } } @@ -234,14 +220,8 @@ namespace VNLib.Net.Http.Core //calculate the number of characters int numChars = encoding.GetCharCount(binBuffer.Span[..read]); - //Guard for overflow - if (((ulong)(numChars + length)) >= int.MaxValue) - { - throw new OverflowException(); - } - - //Re-alloc buffer - charBuffer.ResizeIfSmaller(length + numChars); + //Re-alloc buffer and guard for overflow + charBuffer.ResizeIfSmaller(checked(numChars + length)); //Decode and update position _ = encoding.GetChars(binBuffer.Span[..read], charBuffer.Span.Slice(length, numChars)); @@ -281,14 +261,14 @@ namespace VNLib.Net.Http.Core break; } - //Get header data + //Get header data before the next crlf ReadOnlySpan header = reader.Window[..index]; //Split header at colon - int colon = header.IndexOf(':'); + int colon; //If no data is available after the colon the header is not valid, so move on to the next body - if (colon < 1) + if ((colon = header.IndexOf(':')) < 1) { return; } @@ -299,17 +279,16 @@ namespace VNLib.Net.Http.Core //get the header value ReadOnlySpan headerValue = header[(colon + 1)..]; - //Check for content dispositon header - if (headerType == HttpHelpers.ContentDisposition) + switch (headerType) { - //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()); + case HttpHelpers.ContentDisposition: + //Parse the content dispostion + HttpHelpers.ParseDisposition(headerValue, out DispType, out Name, out FileName); + break; + case HttpRequestHeader.ContentType: + //The header value for content type should be an MIME content type + ctHeaderVal = HttpHelpers.GetContentType(headerValue.Trim()); + break; } //Shift window to the next line @@ -321,19 +300,23 @@ namespace VNLib.Net.Http.Core //If filename is set, this must be a file if (!string.IsNullOrWhiteSpace(FileName)) { - ReadOnlySpan fileData = reader.Window.TrimCRLF(); + //Only add the upload if the request can accept more uploads, otherwise drop it + if (state.Request.CanAddUpload()) + { + ReadOnlySpan fileData = reader.Window.TrimCRLF(); - FileUpload upload = UploadFromString(fileData, state, FileName, ctHeaderVal); + FileUpload upload = UploadFromString(fileData, state, FileName, ctHeaderVal); - //Store the file in the uploads - state.Request.RequestBody.Uploads.Add(upload); + //Store the file in the uploads + state.Request.AddFileUpload(in upload); + } } //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(); + state.Request.RequestArgs[Name] = reader.Window.TrimCRLF().ToString(); } } @@ -387,7 +370,7 @@ namespace VNLib.Net.Http.Core ReadOnlySpan value = kvArg.SliceAfterParam('='); //trim, allocate strings, and store in the request arg dict - Request.RequestBody.RequestArgs[key.TrimCRLF().ToString()] = value.TrimCRLF().ToString(); + Request.RequestArgs[key.TrimCRLF().ToString()] = value.TrimCRLF().ToString(); } /* @@ -404,11 +387,11 @@ namespace VNLib.Net.Http.Core ReadOnlySpan value = queryArgument.SliceAfterParam('='); //Insert into dict - Request.RequestBody.QueryArgs[key.ToString()] = value.ToString(); + Request.QueryArgs[key.ToString()] = value.ToString(); } //if the request has query args, parse and store them - ReadOnlySpan queryString = Request.Location.Query; + ReadOnlySpan queryString = Request.State.Location.Query; if (!queryString.IsEmpty) { diff --git a/lib/Net.Http/src/Core/Request/HttpRequestState.cs b/lib/Net.Http/src/Core/Request/HttpRequestState.cs new file mode 100644 index 0000000..81c2325 --- /dev/null +++ b/lib/Net.Http/src/Core/Request/HttpRequestState.cs @@ -0,0 +1,119 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpRequest.cs +* +* HttpRequest.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.Net; + +namespace VNLib.Net.Http.Core +{ + + /// + /// A mutable http connection state structure that stores HTTP + /// status information + /// + internal struct HttpRequestState + { + /// + /// A value indicating if the client's request had an Expect-100-Continue header + /// + internal bool Expect; + + /// + /// A value that indicates if HTTP keepalive is desired by a client and is respected + /// by the server + /// + internal bool KeepAlive; + + /// + /// A value indicating whether the connection contained a request entity body. + /// + internal bool HasEntityBody; + + /// + /// The connection HTTP version determined by the server. + /// + public HttpVersion HttpVersion; + + /// + /// The requested HTTP method + /// + public HttpMethod Method; + + /// + /// Request wide content type of a request entity body if not using FormData + /// + public ContentType ContentType; + + /// + /// Conent range requested ranges, that are parsed into a start-end tuple + /// + public HttpRange Range; + + /// + /// The number of uploaded files in the request + /// + public int UploadCount; + + /// + /// request's user-agent string + /// + public string? UserAgent; + + /// + /// Boundry header value if reuqest send data using MIME mulit-part form data + /// + public string? Boundry; + + /// + /// Request entity body charset if parsed during content-type parsing + /// + public string? Charset; + + /// + /// The requested resource location url + /// + public Uri Location; + + /// + /// The value of the origin header if one was sent + /// + public Uri? Origin; + + /// + /// The url value of the referer header if one was sent + /// + public Uri? Referrer; + + /// + /// The connection's remote endpoint (ip/port) captured from transport + /// + public IPEndPoint RemoteEndPoint; + + /// + /// The connection's local endpoint (the server's transport socket address) + /// + public IPEndPoint LocalEndPoint; + + } +} \ 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 10697f2..c373310 100644 --- a/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs +++ b/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs @@ -69,9 +69,16 @@ namespace VNLib.Net.Http.Core [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static HttpStatusCode Http1ParseRequestLine(this HttpRequest Request, ref Http1ParseState parseState, ref TransportReader reader, Span lineBuf, bool usingTls) { + /* + * Evil mutable struct, get a local mutable reference to the request's + * state structure in order to initialize state variables. + */ + ref HttpRequestState reqState = ref Request.GetMutableStateForInit(); + //Locals ERRNO requestResult; int index, endloc; + ReadOnlySpan requestLine, pathAndQuery; //Read the start line requestResult = reader.ReadLine(lineBuf); @@ -84,7 +91,7 @@ namespace VNLib.Net.Http.Core } //true up the request line to actual size - ReadOnlySpan requestLine = lineBuf[..(int)requestResult].Trim(); + requestLine = lineBuf[..(int)requestResult].Trim(); //Find the first white space character ("GET / HTTP/1.1") index = requestLine.IndexOf(' '); @@ -92,50 +99,44 @@ namespace VNLib.Net.Http.Core { return HttpStatusCode.BadRequest; } - - //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) + //Decode the verb (function requires the string be the exact characters of the request method) + if ((reqState.Method = HttpHelpers.GetRequestMethod(requestLine[0..index])) == HttpMethod.None) { return HttpStatusCode.MethodNotAllowed; } //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) + if ((endloc = requestLine.LastIndexOf(" HTTP/", StringComparison.OrdinalIgnoreCase)) == -1) { return HttpStatusCode.HttpVersionNotSupported; } - //Try to parse the version and only accept the 3 major versions of http - Request.HttpVersion = HttpHelpers.ParseHttpVersion(requestLine[endloc..]); - //Check to see if the version was parsed succesfully - if (Request.HttpVersion == HttpVersion.None) + //Try to parse the requested http version, only supported versions + if ((reqState.HttpVersion = HttpHelpers.ParseHttpVersion(requestLine[endloc..])) == HttpVersion.None) { //Return not supported return HttpStatusCode.HttpVersionNotSupported; } //Set keepalive flag if http11 - Request.KeepAlive = Request.HttpVersion == HttpVersion.Http11; + reqState.KeepAlive = reqState.HttpVersion == HttpVersion.Http11; //Get the location segment from the request line - ReadOnlySpan paq = requestLine[(index + 1)..endloc].TrimCRLF(); + pathAndQuery = requestLine[(index + 1)..endloc].TrimCRLF(); //Process an absolute uri, - if (paq.Contains("://", StringComparison.Ordinal)) + if (pathAndQuery.Contains("://", StringComparison.Ordinal)) { //Convert the location string to a .net string and init the location builder (will perform validation when the Uri propery is used) - parseState.Location = new(paq.ToString()); + parseState.Location = new(pathAndQuery.ToString()); parseState.IsAbsoluteRequestUrl = true; return 0; } //Try to capture a realative uri - else if (paq.Length > 0 && paq[0] == '/') + else if (pathAndQuery.Length > 0 && pathAndQuery[0] == '/') { //Create a default location uribuilder parseState.Location = new() @@ -145,19 +146,19 @@ namespace VNLib.Net.Http.Core }; //Need to manually parse the query string - int q = paq.IndexOf('?'); + int q = pathAndQuery.IndexOf('?'); //has query? if (q == -1) { - parseState.Location.Path = paq.ToString(); + parseState.Location.Path = pathAndQuery.ToString(); } //Does have query argument else { //separate the path from the query - parseState.Location.Path = paq[0..q].ToString(); - parseState.Location.Query = paq[(q + 1)..].ToString(); + parseState.Location.Path = pathAndQuery[0..q].ToString(); + parseState.Location.Query = pathAndQuery[(q + 1)..].ToString(); } return 0; } @@ -177,6 +178,12 @@ namespace VNLib.Net.Http.Core [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static HttpStatusCode Http1ParseHeaders(this HttpRequest Request, ref Http1ParseState parseState, ref TransportReader reader, in HttpConfig Config, Span lineBuf) { + /* + * Evil mutable struct, get a local mutable reference to the request's + * state structure in order to initialize state variables. + */ + ref HttpRequestState reqState = ref Request.GetMutableStateForInit(); + try { int headerCount = 0, colon; @@ -263,7 +270,7 @@ namespace VNLib.Net.Http.Core case HttpRequestHeader.Connection: { //Update keepalive, if the connection header contains "closed" and with the current value of keepalive - Request.KeepAlive &= !requestHeaderValue.Contains("close", StringComparison.OrdinalIgnoreCase); + reqState.KeepAlive &= !requestHeaderValue.Contains("close", StringComparison.OrdinalIgnoreCase); //Also store the connecion header into the store Request.Headers.Add(HttpRequestHeader.Connection, requestHeaderValue.ToString()); @@ -277,11 +284,11 @@ namespace VNLib.Net.Http.Core return HttpStatusCode.UnsupportedMediaType; } - Request.Boundry = boundry; - Request.Charset = charset; + reqState.Boundry = boundry; + reqState.Charset = charset; //Get the content type enum from mime type - Request.ContentType = HttpHelpers.GetContentType(ct); + reqState.ContentType = HttpHelpers.GetContentType(ct); } break; case HttpRequestHeader.ContentLength: @@ -318,11 +325,11 @@ namespace VNLib.Net.Http.Core //Split the host value by the port parameter ReadOnlySpan 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(); + //Slicing before the colon should always provide a useable hostname, so allocate a string for it + string hostOnly = requestHeaderValue.SliceBeforeParam(':').Trim().ToString(); //Verify that the host is usable - if (Uri.CheckHostName(host) == UriHostNameType.Unknown) + if (Uri.CheckHostName(hostOnly) == UriHostNameType.Unknown) { return HttpStatusCode.BadRequest; } @@ -330,14 +337,14 @@ namespace VNLib.Net.Http.Core //Verify that the host matches the host header if absolue uri is set if (parseState.IsAbsoluteRequestUrl) { - if (!host.Equals(parseState.Location!.Host, StringComparison.OrdinalIgnoreCase)) + if (!hostOnly.Equals(parseState.Location!.Host, StringComparison.OrdinalIgnoreCase)) { return HttpStatusCode.BadRequest; } } //store the host value - parseState.Location!.Host = host; + parseState.Location!.Host = hostOnly; //If the port span is empty, no colon was found or the port is invalid if (!port.IsEmpty) @@ -350,6 +357,9 @@ namespace VNLib.Net.Http.Core //Store port parseState.Location.Port = p; } + + //Set host header in collection also + Request.Headers.Add(HttpRequestHeader.Host, requestHeaderValue.ToString()); } break; case HttpRequestHeader.Cookie: @@ -381,51 +391,85 @@ namespace VNLib.Net.Http.Core //Check the referer header and capture its uri instance, it should be absolutely parseable if (!requestHeaderValue.IsEmpty && Uri.TryCreate(requestHeaderValue.ToString(), UriKind.Absolute, out Uri? refer)) { - Request.Referrer = refer; + reqState.Referrer = refer; } } break; case HttpRequestHeader.Range: { + //Use rfc 7233 -> https://www.rfc-editor.org/rfc/rfc7233 + + //MUST ignore range header if not a GET method + if(reqState.Method != HttpMethod.GET) + { + //Ignore the header and continue parsing headers + break; + } + //See if range bytes value has been set ReadOnlySpan rawRange = requestHeaderValue.SliceAfterParam("bytes=").TrimCRLF(); //Make sure the bytes parameter is set if (rawRange.IsEmpty) { + //Ignore the header and continue parsing headers break; } //Get start range ReadOnlySpan startRange = rawRange.SliceBeforeParam('-'); - //Get end range (empty if no - exists) ReadOnlySpan endRange = rawRange.SliceAfterParam('-'); - //See if a range end is specified - if (endRange.IsEmpty) + //try to parse the range values + bool hasStartRange = ulong.TryParse(startRange, out ulong startRangeValue); + bool hasEndRange = ulong.TryParse(endRange, out ulong endRangeValue); + + /* + * The range header may be a range-from-end type request that + * looks like this: + * + * bytes=-500 + * + * or a range-from-start type request that looks like this: + * bytes=500- + * + * or a full range of bytes + * bytes=0-500 + */ + + if (hasEndRange) { - //No end range specified, so only range start - if (long.TryParse(startRange, out long start) && start > -1) + if (hasStartRange) + { + //Validate explicit range + if(!HttpRange.IsValidRangeValue(startRangeValue, endRangeValue)) + { + //Ignore and continue parsing headers + break; + } + + //Set full http range + reqState.Range = new(startRangeValue, endRangeValue, HttpRangeType.FullRange); + } + else { - //Create new range - Request.Range = new(start, -1); - break; + //From-end range + reqState.Range = new(0, endRangeValue, HttpRangeType.FromEnd); } } - //Range has a start and end - else if (long.TryParse(startRange, out long start) && long.TryParse(endRange, out long end) && end > -1) + else if(hasStartRange) { - //get start and end components from range header - Request.Range = new(start, end); - break; + //Valid start range only, so from start range + reqState.Range = new(startRangeValue, 0, HttpRangeType.FromStart); } + //No valid range values } - //Could not parse start range from header - return HttpStatusCode.RequestedRangeNotSatisfiable; + + break; case HttpRequestHeader.UserAgent: //Store user-agent - Request.UserAgent = requestHeaderValue.IsEmpty ? string.Empty : requestHeaderValue.TrimCRLF().ToString(); + reqState.UserAgent = requestHeaderValue.IsEmpty ? string.Empty : requestHeaderValue.TrimCRLF().ToString(); break; //Special code for origin header case HttpHelpers.Origin: @@ -436,13 +480,13 @@ namespace VNLib.Net.Http.Core //Origin headers should always be absolute address "parsable" if (Uri.TryCreate(origin, UriKind.Absolute, out Uri? org)) { - Request.Origin = org; + reqState.Origin = org; } } break; case HttpRequestHeader.Expect: //Accept 100-continue for the Expect header value - Request.Expect = requestHeaderValue.Equals("100-continue", StringComparison.OrdinalIgnoreCase); + reqState.Expect = requestHeaderValue.Equals("100-continue", StringComparison.OrdinalIgnoreCase); break; default: //By default store the header in the request header store @@ -454,7 +498,7 @@ namespace VNLib.Net.Http.Core } while (true); //If request is http11 then host is required - if (Request.HttpVersion == HttpVersion.Http11 && !hostFound) + if (reqState.HttpVersion == HttpVersion.Http11 && !hostFound) { return HttpStatusCode.BadRequest; } @@ -474,9 +518,9 @@ namespace VNLib.Net.Http.Core { return HttpStatusCode.BadRequest; } - + //Store the finalized location - Request.Location = parseState.Location.Uri; + reqState.Location = parseState.Location.Uri; return 0; } @@ -492,19 +536,25 @@ namespace VNLib.Net.Http.Core [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static HttpStatusCode Http1PrepareEntityBody(this HttpRequest Request, ref Http1ParseState parseState, ref TransportReader reader, in HttpConfig Config) { + /* + * Evil mutable struct, get a local mutable reference to the request's + * state structure in order to initialize state variables. + */ + ref HttpRequestState reqState = ref Request.GetMutableStateForInit(); + //If the content type is multipart, make sure its not too large to ingest - if (Request.ContentType == ContentType.MultiPart && parseState.ContentLength > Config.MaxFormDataUploadSize) + if (reqState.ContentType == ContentType.MultiPart && parseState.ContentLength > Config.MaxFormDataUploadSize) { return HttpStatusCode.RequestEntityTooLarge; } //Only ingest the rest of the message body if the request is not a head, get, or trace methods - if ((Request.Method & (HttpMethod.GET | HttpMethod.HEAD | HttpMethod.TRACE)) != 0) + if ((reqState.Method & (HttpMethod.GET | HttpMethod.HEAD | HttpMethod.TRACE)) != 0) { //Bad format to include a message body with a GET, HEAD, or TRACE request if (parseState.ContentLength > 0) { - Config.ServerLog.Debug("Message body received from {ip} with GET, HEAD, or TRACE request, was considered an error and the request was dropped", Request.RemoteEndPoint); + Config.ServerLog.Debug("Message body received from {ip} with GET, HEAD, or TRACE request, was considered an error and the request was dropped", reqState.RemoteEndPoint); return HttpStatusCode.BadRequest; } else @@ -520,7 +570,7 @@ namespace VNLib.Net.Http.Core if (!transfer.IsEmpty && transfer.Contains("chunked", StringComparison.OrdinalIgnoreCase)) { //Not a valid http version for chunked transfer encoding - if (Request.HttpVersion != HttpVersion.Http11) + if (reqState.HttpVersion != HttpVersion.Http11) { return HttpStatusCode.BadRequest; } @@ -533,7 +583,7 @@ namespace VNLib.Net.Http.Core */ if (parseState.ContentLength > 0) { - Config.ServerLog.Debug("Possible attempted desync, Content length + chunked encoding specified. RemoteEP: {ip}", Request.RemoteEndPoint); + Config.ServerLog.Debug("Possible attempted desync, Content length + chunked encoding specified. RemoteEP: {ip}", reqState.RemoteEndPoint); return HttpStatusCode.BadRequest; } @@ -566,7 +616,8 @@ namespace VNLib.Net.Http.Core _ = reader.ReadRemaining(initData.Value.DataSegment); } - Request.HasEntityBody = true; + //Notify request that an entity body has been set + reqState.HasEntityBody = true; } //Success! return 0; diff --git a/lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs b/lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs index f87ec98..5e7be39 100644 --- a/lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs +++ b/lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs @@ -23,94 +23,59 @@ */ using System; +using System.Diagnostics; +using System.Runtime.InteropServices; using VNLib.Utils.IO; +using VNLib.Utils.Memory; + using VNLib.Net.Http.Core.Buffering; namespace VNLib.Net.Http.Core.Response { + /// /// A specialized for buffering data /// in Http/1.1 chunks /// - internal class ChunkDataAccumulator : IDataAccumulator + internal readonly struct ChunkDataAccumulator { - public const int RESERVED_CHUNK_SUGGESTION = 32; - - private readonly int ReservedSize; + /* + * The number of bytes to reserve at the beginning of the buffer + * for the chunk size segment. This is the maximum size of the + */ + public const int ReservedSize = 16; + private readonly IHttpContextInformation Context; private readonly IChunkAccumulatorBuffer Buffer; - + + /* + * Must always leave enough room for trailing crlf at the end of + * the buffer + */ + private readonly int TotalMaxBufferSize => Buffer.Size - (int)Context.CrlfSegment.Length; public ChunkDataAccumulator(IChunkAccumulatorBuffer buffer, IHttpContextInformation context) { - ReservedSize = RESERVED_CHUNK_SUGGESTION; - Context = context; Buffer = buffer; } - - /* - * Reserved offset is a pointer to the first byte of the reserved chunk window - * that actually contains the size segment data. - */ - - private int _reservedOffset; - - /// - public int RemainingSize => Buffer.Size - AccumulatedSize; - - /// - Span IDataAccumulator.Remaining => Buffer.GetBinSpan()[AccumulatedSize..]; - - /// - Span IDataAccumulator.Accumulated => Buffer.GetBinSpan()[_reservedOffset.. AccumulatedSize]; - - /// - public int AccumulatedSize { get; set; } - - /// - public void Advance(int count) => AccumulatedSize += count; - - /// - /// Gets the remaining segment of the buffer to write chunk data to. - /// - /// The chunk buffer to write data to - public Memory GetRemainingSegment() - { - /* - * We need to return the remaining segment of the buffer, the segment after the - * accumulated chunk data, but before the trailing crlf. - */ - - //Get the remaining buffer segment - return Buffer.GetMemory().Slice(AccumulatedSize, RemainingSize - Context.EncodedSegments.CrlfBytes.Length); - } - - /// - /// Calculates the usable remaining size of the chunk buffer. - /// - /// The number of bytes remaining in the buffer - public int GetRemainingSegmentSize() - { - //Remaining size accounting for the trailing crlf - return RemainingSize - Context.EncodedSegments.CrlfBytes.Length; - } /// /// Complets and returns the memory segment containing the chunk data to send /// to the client. This also resets the accumulator. /// /// - public Memory GetChunkData() + public readonly Memory GetChunkData(int accumulatedSize) { //Update the chunk size - UpdateChunkSize(); + int reservedOffset = UpdateChunkSize(Buffer, Context, accumulatedSize); + int endPtr = GetPointerToEndOfUsedBuffer(accumulatedSize); //Write trailing chunk delimiter - this.Append(Context.EncodedSegments.CrlfBytes.Span); + endPtr += Context.CrlfSegment.DangerousCopyTo(Buffer, endPtr); - return GetCompleteChunk(); + return Buffer.GetMemory()[reservedOffset..endPtr]; } /// @@ -118,47 +83,59 @@ namespace VNLib.Net.Http.Core.Response /// to the client. /// /// - public Memory GetFinalChunkData() + public readonly Memory GetFinalChunkData(int accumulatedSize) { //Update the chunk size - UpdateChunkSize(); + int reservedOffset = UpdateChunkSize(Buffer, Context, accumulatedSize); + int endPtr = GetPointerToEndOfUsedBuffer(accumulatedSize); //Write trailing chunk delimiter - this.Append(Context.EncodedSegments.CrlfBytes.Span); + endPtr += Context.CrlfSegment.DangerousCopyTo(Buffer, endPtr); //Write final chunk to the end of the accumulator - this.Append(Context.EncodedSegments.FinalChunkTermination.Span); + endPtr += Context.FinalChunkSegment.DangerousCopyTo(Buffer, endPtr); - return GetCompleteChunk(); + return Buffer.GetMemory()[reservedOffset..endPtr]; } + /// + /// Gets the remaining segment of the buffer to write chunk data to. + /// + /// The chunk buffer to write data to + public readonly Memory GetRemainingSegment(int accumulatedSize) + { + int endOfDataOffset = GetPointerToEndOfUsedBuffer(accumulatedSize); + return Buffer.GetMemory()[endOfDataOffset..TotalMaxBufferSize]; + } + + /// + /// Calculates the usable remaining size of the chunk buffer. + /// + /// The number of bytes remaining in the buffer + public readonly int GetRemainingSegmentSize(int accumulatedSize) + => TotalMaxBufferSize - GetPointerToEndOfUsedBuffer(accumulatedSize); + /* * 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. + * The accumulated data position is the number of chunk bytes accumulated + * in the data segment. This does not include the number of reserved bytes + * are before it. + * + * We can get the value that points to the end of the used buffer + * and use the memory range operator to get the segment from the reserved + * segment, to the actual end of the data segment. */ - private Memory GetCompleteChunk() => Buffer.GetMemory()[_reservedOffset..AccumulatedSize]; - - - private void InitReserved() + private readonly Memory GetCompleteChunk(int reservedOffset, int accumulatedSize) { - //First reserve the chunk window by advancing the accumulator to the reserved size - Advance(ReservedSize); - } - - /// - public void Reset() - { - //zero offsets - _reservedOffset = 0; - AccumulatedSize = 0; - //Init reserved segment - InitReserved(); + return Buffer.GetMemory()[reservedOffset..accumulatedSize]; } + + private static int GetPointerToEndOfUsedBuffer(int accumulatedSize) => accumulatedSize + ReservedSize; + /* * UpdateChunkSize method updates the running total of the chunk size * in the reserved segment of the buffer. This is because http chunking @@ -177,29 +154,32 @@ namespace VNLib.Net.Http.Core.Response * [...0a\r\n] [10 bytes of data] [eoc] */ - private void UpdateChunkSize() + private static int UpdateChunkSize(IChunkAccumulatorBuffer buffer, IHttpContextInformation context, int chunkSize) { - const int CharBufSize = 2 * sizeof(int); + const int CharBufSize = 2 * sizeof(int) + 2; //2 hex chars per byte + crlf /* * Alloc stack buffer to store chunk size hex chars * the size of the buffer should be at least the number * of bytes of the max chunk size */ - Span s = stackalloc char[CharBufSize]; - - //Chunk size is the accumulated size without the reserved segment - int chunkSize = (AccumulatedSize - ReservedSize); + Span intFormatBuffer = stackalloc char[CharBufSize]; + + + //temp buffer to store binary encoded data in + Span chunkSizeBinBuffer = stackalloc byte[ReservedSize]; //format the chunk size - chunkSize.TryFormat(s, out int written, "x"); + bool formatSuccess = chunkSize.TryFormat(intFormatBuffer, out int bytesFormatted, "x", null); + Debug.Assert(formatSuccess, "Failed to write integer chunk size to temp buffer"); - //temp buffer to store encoded data in - Span encBuf = stackalloc byte[ReservedSize]; - //Encode the chunk size chars - int initOffset = Context.Encoding.GetBytes(s[..written], encBuf); + //Write the trailing crlf to the end of the encoded chunk size + intFormatBuffer[bytesFormatted++] = '\r'; + intFormatBuffer[bytesFormatted++] = '\n'; - Span encoded = encBuf[..initOffset]; + //Encode the chunk size chars + int totalChunkBufferBytes = context.Encoding.GetBytes(intFormatBuffer[..bytesFormatted], chunkSizeBinBuffer); + Debug.Assert(totalChunkBufferBytes <= ReservedSize, "Chunk size buffer offset is greater than reserved size. Encoding failure"); /* * We need to calcuate how to store the encoded buffer directly @@ -209,31 +189,15 @@ namespace VNLib.Net.Http.Core.Response * the exact size required to store the encoded chunk size */ - _reservedOffset = (ReservedSize - (initOffset + Context.EncodedSegments.CrlfBytes.Length)); - - Span upshifted = Buffer.GetBinSpan()[_reservedOffset..ReservedSize]; + int reservedOffset = ReservedSize - totalChunkBufferBytes; - //First write the chunk size - encoded.CopyTo(upshifted); + //Copy encoded chunk size to the reserved segment + ref byte reservedSegRef = ref buffer.DangerousGetBinRef(reservedOffset); + ref byte chunkSizeBufRef = ref MemoryMarshal.GetReference(chunkSizeBinBuffer); - //Upshift again to write the crlf - upshifted = upshifted[initOffset..]; + MemoryUtil.Memmove(ref chunkSizeBufRef, 0, ref reservedSegRef, 0, (uint)totalChunkBufferBytes); - //Copy crlf - Context.EncodedSegments.CrlfBytes.Span.CopyTo(upshifted); - } - - - public void OnNewRequest() - { - InitReserved(); - } - - public void OnComplete() - { - //Zero offsets - _reservedOffset = 0; - AccumulatedSize = 0; + return reservedOffset; } } } \ 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 deleted file mode 100644 index 267d8ed..0000000 --- a/lib/Net.Http/src/Core/Response/ChunkedStream.cs +++ /dev/null @@ -1,97 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Net.Http -* File: ChunkedStream.cs -* -* ChunkedStream.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/. -*/ - -/* -* Provides a Chunked data-encoding stream for writing data-chunks to -* the transport using the basic chunked encoding format from MDN -* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding#directives -*/ - -using System; -using System.Threading; -using System.Threading.Tasks; - -using VNLib.Net.Http.Core.Buffering; - -namespace VNLib.Net.Http.Core.Response -{ - - /// - /// Writes chunked HTTP message bodies to an underlying streamwriter - /// - internal sealed class ChunkedStream : ReusableResponseStream, IResponseDataWriter - { - private readonly ChunkDataAccumulator ChunckAccumulator; - - internal ChunkedStream(IChunkAccumulatorBuffer buffer, IHttpContextInformation context) - { - //Init accumulator - ChunckAccumulator = new(buffer, context); - } - - #region Hooks - - /// - public void OnNewRequest() - { - ChunckAccumulator.OnNewRequest(); - } - - /// - public void OnComplete() - { - ChunckAccumulator.OnComplete(); - } - - /// - public Memory GetMemory() => ChunckAccumulator.GetRemainingSegment(); - - /// - public int Advance(int written) - { - //Advance the accumulator - ChunckAccumulator.Advance(written); - return ChunckAccumulator.GetRemainingSegmentSize(); - } - - /// - public ValueTask FlushAsync(bool isFinal) - { - /* - * We need to know when the final chunk is being flushed so we can - * write the final termination sequence to the transport. - */ - - Memory chunkData = isFinal ? ChunckAccumulator.GetFinalChunkData() : ChunckAccumulator.GetChunkData(); - - //Reset the accumulator - ChunckAccumulator.Reset(); - - //Write remaining data to stream - return transport!.WriteAsync(chunkData, CancellationToken.None); - } - - #endregion - } -} \ No newline at end of file diff --git a/lib/Net.Http/src/Core/Response/DirectStream.cs b/lib/Net.Http/src/Core/Response/DirectStream.cs deleted file mode 100644 index 2406f0f..0000000 --- a/lib/Net.Http/src/Core/Response/DirectStream.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Net.Http -* File: DirectStream.cs -* -* DirectStream.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.Threading.Tasks; - -namespace VNLib.Net.Http.Core.Response -{ - - internal sealed class DirectStream : ReusableResponseStream, IDirectResponsWriter - { - /// - public Task FlushAsync() => transport!.FlushAsync(); - - /// - public ValueTask WriteAsync(ReadOnlyMemory buffer) => transport!.WriteAsync(buffer); - } -} diff --git a/lib/Net.Http/src/Core/Response/HeaderDataAccumulator.cs b/lib/Net.Http/src/Core/Response/HeaderDataAccumulator.cs index 7f026e2..0611095 100644 --- a/lib/Net.Http/src/Core/Response/HeaderDataAccumulator.cs +++ b/lib/Net.Http/src/Core/Response/HeaderDataAccumulator.cs @@ -26,6 +26,7 @@ using System; using VNLib.Utils.Memory; using VNLib.Utils.Extensions; + using VNLib.Net.Http.Core.Buffering; namespace VNLib.Net.Http.Core.Response @@ -34,11 +35,10 @@ namespace VNLib.Net.Http.Core.Response /// /// Specialized data accumulator for compiling response headers /// - internal sealed class HeaderDataAccumulator + internal readonly struct HeaderDataAccumulator { private readonly IResponseHeaderAccBuffer _buffer; private readonly IHttpContextInformation _contextInfo; - private int AccumulatedSize; public HeaderDataAccumulator(IResponseHeaderAccBuffer accBuffer, IHttpContextInformation ctx) { @@ -46,11 +46,17 @@ namespace VNLib.Net.Http.Core.Response _contextInfo = ctx; } + /// + /// Gets the accumulated response data as its memory buffer, and resets the internal accumulator + /// + /// The buffer segment containing the accumulated response data + public readonly Memory GetResponseData(int accumulatedSize) => _buffer.GetMemory()[..accumulatedSize]; + /// /// Initializes a new for buffering character header data /// /// A for buffering character header data - public ForwardOnlyWriter GetWriter() + public readonly ForwardOnlyWriter GetWriter() { Span chars = _buffer.GetCharSpan(); return new ForwardOnlyWriter(chars); @@ -60,7 +66,8 @@ namespace VNLib.Net.Http.Core.Response /// Encodes and writes the contents of the to the internal accumulator /// /// The character buffer writer to commit data from - public void CommitChars(ref ForwardOnlyWriter writer) + /// A reference to the cumulative number of bytes written to the buffer + public readonly void CommitChars(ref ForwardOnlyWriter writer, ref int accumulatedSize) { if (writer.Written == 0) { @@ -68,54 +75,28 @@ namespace VNLib.Net.Http.Core.Response } //Write the entire token to the buffer - WriteToken(writer.AsSpan()); + WriteToken(writer.AsSpan(), ref accumulatedSize); } /// /// Encodes a single token and writes it directly to the internal accumulator /// /// The character sequence to accumulate - public void WriteToken(ReadOnlySpan chars) + /// A reference to the cumulative number of bytes written to the buffer + public readonly void WriteToken(ReadOnlySpan chars, ref int accumulatedSize) { //Get remaining buffer - Span remaining = _buffer.GetBinSpan()[AccumulatedSize..]; + Span remaining = _buffer.GetBinSpan(accumulatedSize); //Commit all chars to the buffer - AccumulatedSize += _contextInfo.Encoding.GetBytes(chars, remaining); + accumulatedSize += _contextInfo.Encoding.GetBytes(chars, remaining); } /// /// Writes the http termination sequence to the internal accumulator /// - public void WriteTermination() - { - //Write the http termination sequence - Span remaining = _buffer.GetBinSpan()[AccumulatedSize..]; - - _contextInfo.EncodedSegments.CrlfBytes.Span.CopyTo(remaining); + public readonly void WriteTermination(ref int accumulatedSize) + => accumulatedSize += _contextInfo.CrlfSegment.DangerousCopyTo(_buffer, accumulatedSize); - //Advance the accumulated window - AccumulatedSize += _contextInfo.EncodedSegments.CrlfBytes.Length; - } - - /// - /// Resets the internal accumulator - /// - public void Reset() => AccumulatedSize = 0; - - /// - /// Gets the accumulated response data as its memory buffer, and resets the internal accumulator - /// - /// The buffer segment containing the accumulated response data - public Memory GetResponseData() - { - //get the current buffer as memory and return the accumulated segment - Memory accumulated = _buffer.GetMemory()[..AccumulatedSize]; - - //Reset the buffer - Reset(); - - return accumulated; - } } } \ No newline at end of file diff --git a/lib/Net.Http/src/Core/Response/HttpContextExtensions.cs b/lib/Net.Http/src/Core/Response/HttpContextExtensions.cs index 12702b3..b3ca1c0 100644 --- a/lib/Net.Http/src/Core/Response/HttpContextExtensions.cs +++ b/lib/Net.Http/src/Core/Response/HttpContextExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -26,10 +26,8 @@ using System; using System.Net; using System.Runtime.CompilerServices; -using VNLib.Utils.Memory; -using VNLib.Utils.Extensions; -namespace VNLib.Net.Http.Core +namespace VNLib.Net.Http.Core.Response { /// /// Provides extended funcionality of an @@ -59,7 +57,7 @@ namespace VNLib.Net.Http.Core public const string NO_CACHE_STRING = "no-cache"; private static readonly string CACHE_CONTROL_VALUE = HttpHelpers.GetCacheString(CacheType.NoCache | CacheType.NoStore); - + /// /// Sets CacheControl and Pragma headers to no-cache /// @@ -70,29 +68,6 @@ namespace VNLib.Net.Http.Core Response.Headers[HttpResponseHeader.CacheControl] = CACHE_CONTROL_VALUE; } - /// - /// Sets the content-range header to the specified parameters - /// - /// - /// The content range start - /// The content range end - /// The total content length - public static void SetContentRange(this HttpResponse Response, long start, long end, long length) - { - //Alloc enough space to hold the string - Span buffer = stackalloc char[64]; - ForwardOnlyWriter rangeBuilder = new(buffer); - //Build the range header in this format "bytes -/" - rangeBuilder.Append("bytes "); - rangeBuilder.Append(start); - rangeBuilder.Append('-'); - rangeBuilder.Append(end); - rangeBuilder.Append('/'); - rangeBuilder.Append(length); - //Print to a string and set the content range header - Response.Headers[HttpResponseHeader.ContentRange] = rangeBuilder.ToString(); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static ReadOnlyMemory GetRemainingConstrained(this IMemoryResponseReader reader, int limit) { @@ -101,24 +76,5 @@ namespace VNLib.Net.Http.Core //get segment and slice return reader.GetMemory()[..size]; } - - /// - /// If an end-range is set, returns the remaining bytes up to the end-range, otherwise returns the entire request body length - /// - /// - /// The data range - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static long GetResponseLengthWithRange(this IHttpResponseBody body, Tuple range) - { - /* - * If end range is defined, then calculate the length of the response - * - * The length is the end range minus the start range plus 1 because range - * is an inclusve value - */ - - return range.Item2 < 0 ? body.Length : Math.Min(body.Length, range.Item2 - range.Item1 + 1); - } } } \ 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 ca5f040..98c6043 100644 --- a/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs +++ b/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs @@ -64,6 +64,17 @@ namespace VNLib.Net.Http.Core } else { + //Set implicit 0 content length if not disabled + if (!ContextFlags.IsSet(HttpControlMask.ImplictContentLengthDisabled)) + { + //RFC 7230, length only set on 200 + but not 204 + if ((int)Response.StatusCode >= 200 && (int)Response.StatusCode != 204) + { + //If headers havent been sent by this stage there is no content, so set length to 0 + Response.Headers.Set(HttpResponseHeader.ContentLength, "0"); + } + } + await discardTask; } @@ -79,88 +90,74 @@ namespace VNLib.Net.Http.Core //Adjust/append vary header Response.Headers.Add(HttpResponseHeader.Vary, "Accept-Encoding"); - bool hasRange = Request.Range != null; long length = ResponseBody.Length; CompressionMethod compMethod = CompressionMethod.None; - - /* - * Process range header, data will not be compressed because that would - * require buffering, not a feature yet, and since the range will tell - * us the content length, we can get a direct stream to write to - */ - if (hasRange) - { - //Get local range - Tuple range = Request.Range!; - - //Calc constrained content length - length = ResponseBody.GetResponseLengthWithRange(range); - - //End range is inclusive so substract 1 - long endRange = (range.Item1 + length) - 1; - - //Set content-range header - Response.SetContentRange(range.Item1, endRange, length); - } /* * It will be known at startup whether compression is supported, if not this is * essentially a constant. */ - else if (ParentServer.SupportedCompressionMethods != CompressionMethod.None) + if (ParentServer.SupportedCompressionMethods != CompressionMethod.None) { //Determine if compression should be used bool compressionDisabled = //disabled because app code disabled it - ContextFlags.IsSet(COMPRESSION_DISABLED_MSK) + ContextFlags.IsSet(HttpControlMask.CompressionDisabed) //Disabled because too large or too small - || ResponseBody.Length >= ParentServer.Config.CompressionLimit - || ResponseBody.Length < ParentServer.Config.CompressionMinimum + || length >= ParentServer.Config.CompressionLimit + || length < ParentServer.Config.CompressionMinimum //Disabled because lower than http11 does not support chunked encoding - || Request.HttpVersion < HttpVersion.Http11; + || Request.State.HttpVersion < HttpVersion.Http11; if (!compressionDisabled) { //Get first compression method or none if disabled compMethod = Request.GetCompressionSupport(ParentServer.SupportedCompressionMethods); - //Set response headers + //Set response compression encoding headers switch (compMethod) { case CompressionMethod.Gzip: - //Specify gzip encoding (using chunked encoding) - Response.Headers[HttpResponseHeader.ContentEncoding] = "gzip"; + Response.Headers.Set(HttpResponseHeader.ContentEncoding, "gzip"); break; case CompressionMethod.Deflate: - //Specify delfate encoding (using chunked encoding) - Response.Headers[HttpResponseHeader.ContentEncoding] = "deflate"; + Response.Headers.Set(HttpResponseHeader.ContentEncoding, "deflate"); break; case CompressionMethod.Brotli: - //Specify Brotli encoding (using chunked encoding) - Response.Headers[HttpResponseHeader.ContentEncoding] = "br"; + Response.Headers.Set(HttpResponseHeader.ContentEncoding, "br"); break; } } } //Check on head methods - if (Request.Method == HttpMethod.HEAD) + if (Request.State.Method == HttpMethod.HEAD) { //Specify what the content length would be - Response.Headers[HttpResponseHeader.ContentLength] = length.ToString(); + Response.Headers.Set(HttpResponseHeader.ContentLength, length.ToString()); //We must send headers here so content length doesnt get overwritten, close will be called after this to flush to transport Response.FlushHeaders(); return Task.CompletedTask; } + /* + * User submitted a 0 length response body, let hooks clean-up + * any resources. Simply flush headers and exit + */ + else if(length == 0) + { + + Response.FlushHeaders(); + return Task.CompletedTask; + } else { //Set the explicit length if a range was set - return WriteEntityDataAsync(length, compMethod, hasRange); + return WriteEntityDataAsync(length, compMethod); } } - private async Task WriteEntityDataAsync(long length, CompressionMethod compMethod, bool hasExplicitLength) + private async Task WriteEntityDataAsync(long length, CompressionMethod compMethod) { //Determine if buffer is required Memory buffer = ResponseBody.BufferRequired ? Buffers.GetResponseDataBuffer() : Memory.Empty; @@ -170,11 +167,11 @@ namespace VNLib.Net.Http.Core if (compMethod == CompressionMethod.None) { - //Setup a direct stream to write to + //Setup a direct stream to write to because compression is not enabled IDirectResponsWriter output = Response.GetDirectStream(); //Write response with optional forced length - await ResponseBody.WriteEntityAsync(output, hasExplicitLength ? length : -1, buffer); + await ResponseBody.WriteEntityAsync(output, buffer); } else { diff --git a/lib/Net.Http/src/Core/Response/HttpResponse.cs b/lib/Net.Http/src/Core/Response/HttpResponse.cs index 90f6f24..3f2ae56 100644 --- a/lib/Net.Http/src/Core/Response/HttpResponse.cs +++ b/lib/Net.Http/src/Core/Response/HttpResponse.cs @@ -25,6 +25,7 @@ using System; using System.IO; using System.Net; +using System.Threading; using System.Diagnostics; using System.Threading.Tasks; using System.Collections.Generic; @@ -34,27 +35,26 @@ using VNLib.Utils; using VNLib.Utils.IO; using VNLib.Utils.Memory; using VNLib.Utils.Extensions; - using VNLib.Net.Http.Core.Buffering; -using VNLib.Net.Http.Core.Response; -namespace VNLib.Net.Http.Core +namespace VNLib.Net.Http.Core.Response { internal sealed class HttpResponse : IHttpLifeCycle #if DEBUG - ,IStringSerializeable + , IStringSerializeable #endif { + private readonly IHttpContextInformation ContextInfo; private readonly HashSet Cookies; - private readonly HeaderDataAccumulator Writer; - private readonly DirectStream ReusableDirectStream; private readonly ChunkedStream ReusableChunkedStream; - private readonly IHttpContextInformation ContextInfo; + private readonly HeaderDataAccumulator Writer; + + private int _headerWriterPosition; private bool HeadersSent; private bool HeadersBegun; - + private HttpStatusCode _code; /// @@ -62,7 +62,12 @@ namespace VNLib.Net.Http.Core /// public VnWebHeaderCollection Headers { get; } - public HttpResponse(IHttpBufferManager manager, IHttpContextInformation ctx) + /// + /// The current http status code value + /// + internal HttpStatusCode StatusCode => _code; + + public HttpResponse(IHttpContextInformation ctx, IHttpBufferManager manager) { ContextInfo = ctx; @@ -70,11 +75,11 @@ namespace VNLib.Net.Http.Core Headers = new(); Cookies = new(); - //Create a new reusable writer stream - Writer = new(manager.ResponseHeaderBuffer, ctx); - + //Init header accumulator + Writer = new(manager.ResponseHeaderBuffer, ContextInfo); + //Create a new chunked stream - ReusableChunkedStream = new(manager.ChunkAccumulatorBuffer, ctx); + ReusableChunkedStream = new(manager.ChunkAccumulatorBuffer, ContextInfo); ReusableDirectStream = new(); } @@ -89,7 +94,7 @@ namespace VNLib.Net.Http.Core { throw new InvalidOperationException("Status code has already been sent"); } - + _code = code; } @@ -147,7 +152,7 @@ namespace VNLib.Net.Http.Core foreach (HttpCookie cookie in Cookies) { writer.Append("Set-Cookie: "); - + //Write the cookie to the header buffer cookie.Compile(ref writer); @@ -159,7 +164,7 @@ namespace VNLib.Net.Http.Core } //Commit headers - Writer.CommitChars(ref writer); + Writer.CommitChars(ref writer, ref _headerWriterPosition); } private ValueTask EndFlushHeadersAsync() @@ -168,10 +173,10 @@ namespace VNLib.Net.Http.Core FlushHeaders(); //Last line to end headers - Writer.WriteTermination(); + Writer.WriteTermination(ref _headerWriterPosition); //Get the response data header block - Memory responseBlock = Writer.GetResponseData(); + Memory responseBlock = Writer.GetResponseData(_headerWriterPosition); //Update sent headers HeadersSent = true; @@ -202,13 +207,13 @@ namespace VNLib.Net.Http.Core if (contentLength < 0) { //Add chunked header - Headers[HttpResponseHeader.TransferEncoding] = "chunked"; - + Headers.Set(HttpResponseHeader.TransferEncoding, "chunked"); + } else { //Add content length header - Headers[HttpResponseHeader.ContentLength] = contentLength.ToString(); + Headers.Set(HttpResponseHeader.ContentLength, contentLength.ToString()); } //Flush headers @@ -223,8 +228,8 @@ namespace VNLib.Net.Http.Core public IDirectResponsWriter GetDirectStream() { //Headers must be sent before getting a direct stream - Debug.Assert(HeadersSent); - return ReusableDirectStream; + Debug.Assert(HeadersSent, "A call to stream capture was made before the headers were flushed to the transport"); + return ReusableDirectStream; } /// @@ -241,7 +246,7 @@ namespace VNLib.Net.Http.Core return ReusableChunkedStream; } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] void Check() { @@ -262,13 +267,16 @@ namespace VNLib.Net.Http.Core Check(); //Send a status message with the continue response status - Writer.WriteToken(HttpHelpers.GetResponseString(ContextInfo.CurrentVersion, HttpStatusCode.Continue)); + Writer.WriteToken(HttpHelpers.GetResponseString(ContextInfo.CurrentVersion, HttpStatusCode.Continue), ref _headerWriterPosition); //Trailing crlf - Writer.WriteTermination(); + Writer.WriteTermination(ref _headerWriterPosition); //Get the response data header block - Memory responseBlock = Writer.GetResponseData(); + Memory responseBlock = Writer.GetResponseData(_headerWriterPosition); + + //reset after getting the written buffer + _headerWriterPosition = 0; //get base stream Stream bs = ContextInfo.GetTransport(); @@ -290,13 +298,6 @@ namespace VNLib.Net.Http.Core //If headers haven't been sent yet, send them and there must be no content 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"; - } - //Finalize headers return EndFlushHeadersAsync(); } @@ -332,8 +333,9 @@ namespace VNLib.Net.Http.Core { //Default to okay status code _code = HttpStatusCode.OK; - - ReusableChunkedStream.OnNewRequest(); + + //Set new header writer on every new request + _headerWriterPosition = 0; } /// @@ -347,13 +349,73 @@ namespace VNLib.Net.Http.Core HeadersBegun = false; HeadersSent = false; - //Reset header writer - Writer.Reset(); + //clear header writer + _headerWriterPosition = 0; //Call child lifecycle hooks ReusableChunkedStream.OnComplete(); } + private sealed class DirectStream : ReusableResponseStream, IDirectResponsWriter + { + /// + public ValueTask WriteAsync(ReadOnlyMemory buffer) => transport!.WriteAsync(buffer); + } + + /// + /// Writes chunked HTTP message bodies to an underlying streamwriter + /// + private sealed class ChunkedStream : ReusableResponseStream, IResponseDataWriter + { + private readonly ChunkDataAccumulator _chunkAccumulator; + + /* + * Tracks the number of bytes accumulated in the + * current chunk. + */ + private int _accumulatedBytes; + + public ChunkedStream(IChunkAccumulatorBuffer buffer, IHttpContextInformation context) + => _chunkAccumulator = new(buffer, context); + + #region Hooks + + /// + public void OnComplete() => _accumulatedBytes = 0; + + /// + public Memory GetMemory() => _chunkAccumulator.GetRemainingSegment(_accumulatedBytes); + + /// + public int Advance(int written) + { + //Advance the accumulator + _accumulatedBytes += written; + return _chunkAccumulator.GetRemainingSegmentSize(_accumulatedBytes); + } + + /// + public ValueTask FlushAsync(bool isFinal) + { + /* + * We need to know when the final chunk is being flushed so we can + * write the final termination sequence to the transport. + */ + + Memory chunkData = isFinal ? + _chunkAccumulator.GetFinalChunkData(_accumulatedBytes) : + _chunkAccumulator.GetChunkData(_accumulatedBytes); + + //Reset accumulator + _accumulatedBytes = 0; + + //Write remaining data to stream + return transport!.WriteAsync(chunkData, CancellationToken.None); + } + + #endregion + } + #if DEBUG public override string ToString() => Compile(); @@ -364,7 +426,7 @@ namespace VNLib.Net.Http.Core using IMemoryHandle buffer = MemoryUtil.SafeAlloc(16 * 1024); //Writer - ForwardOnlyWriter writer = new (buffer.Span); + ForwardOnlyWriter writer = new(buffer.Span); Compile(ref writer); return writer.ToString(); } diff --git a/lib/Net.Http/src/Core/Response/IDirectResponsWriter.cs b/lib/Net.Http/src/Core/Response/IDirectResponsWriter.cs index 7c9ca41..bb28038 100644 --- a/lib/Net.Http/src/Core/Response/IDirectResponsWriter.cs +++ b/lib/Net.Http/src/Core/Response/IDirectResponsWriter.cs @@ -39,11 +39,5 @@ namespace VNLib.Net.Http.Core.Response /// The response data to write /// A value task that resolves when the write operation is complete ValueTask WriteAsync(ReadOnlyMemory buffer); - - /// - /// Flushes any remaining data to the client - /// - /// A task that resolves when the flush operationis complete - Task FlushAsync(); } } diff --git a/lib/Net.Http/src/Core/Response/ResponseWriter.cs b/lib/Net.Http/src/Core/Response/ResponseWriter.cs index b6b2488..75ef790 100644 --- a/lib/Net.Http/src/Core/Response/ResponseWriter.cs +++ b/lib/Net.Http/src/Core/Response/ResponseWriter.cs @@ -31,49 +31,48 @@ using System; using System.IO; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using System.Runtime.CompilerServices; using VNLib.Utils.Memory; -using VNLib.Net.Http.Core.Response; using VNLib.Net.Http.Core.Compression; -namespace VNLib.Net.Http.Core +namespace VNLib.Net.Http.Core.Response { internal sealed class ResponseWriter : IHttpResponseBody { - private Stream? _streamResponse; - private IMemoryResponseReader? _memoryResponse; - + private ResponsBodyDataState _userState; + /// - public bool HasData { get; private set; } + public bool HasData => _userState.IsSet; //Buffering is required when a stream is set - bool IHttpResponseBody.BufferRequired => _streamResponse != null; + /// + public bool BufferRequired => _userState.Stream != null; /// - public long Length { get; private set; } + public long Length => _userState.Legnth; /// /// Attempts to set the response body as a stream /// /// The stream response body to read + /// Explicit length of the stream /// True if the response entity could be set, false if it has already been set - internal bool TrySetResponseBody(Stream response) + internal bool TrySetResponseBody(Stream response, long length) { - if (HasData) + if (_userState.IsSet) { return false; } - //Get relative length of the stream, IE the remaning bytes in the stream if position has been modified - Length = (response.Length - response.Position); - //Store ref to stream - _streamResponse = response; - //update has-data flag - HasData = true; + Debug.Assert(response != null, "Stream value is null, illegal operation"); + Debug.Assert(length > -1, "explicit length passed a negative value, illegal operation"); + + _userState = new(response, length); return true; } @@ -84,138 +83,90 @@ namespace VNLib.Net.Http.Core /// True if the response entity could be set, false if it has already been set internal bool TrySetResponseBody(IMemoryResponseReader response) { - if (HasData) + if (_userState.IsSet) { return false; } - //Get length - Length = response.Remaining; - //Store ref to stream - _memoryResponse = response; - //update has-data flag - HasData = true; + Debug.Assert(response != null, "Memory response argument was null and expected a value"); + + //Assign user-state + _userState = new(response); return true; } -#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + private ReadOnlyMemory _readSegment; + private ForwardOnlyMemoryReader _streamReader; - ReadOnlyMemory _readSegment; +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task /// - async Task IHttpResponseBody.WriteEntityAsync(IDirectResponsWriter dest, long count, Memory buffer) + public async Task WriteEntityAsync(IDirectResponsWriter dest, Memory buffer) { - int remaining; - //Write a sliding window response - if (_memoryResponse != null) + if (_userState.MemResponse != null) { - if(count > 0) + //Write response body from memory + while (_userState.MemResponse.Remaining > 0) { - //Get min value from count/range length - remaining = (int)Math.Min(count, _memoryResponse.Remaining); - - //Write response body from memory - while (remaining > 0) - { - //Get remaining segment - _readSegment = _memoryResponse.GetRemainingConstrained(remaining); - - //Write segment to output stream - await dest.WriteAsync(_readSegment); + //Get remaining segment + _readSegment = _userState.MemResponse.GetMemory(); - //Advance by the written ammount - _memoryResponse.Advance(_readSegment.Length); + //Write segment to output stream + await dest.WriteAsync(_readSegment); - //Update remaining - remaining -= _readSegment.Length; - } - } - else - { - //Write response body from memory - while (_memoryResponse.Remaining > 0) - { - //Get remaining segment - _readSegment = _memoryResponse.GetMemory(); - - //Write segment to output stream - await dest.WriteAsync(_readSegment); - - //Advance by the written amount - _memoryResponse.Advance(_readSegment.Length); - } + //Advance by the written amount + _userState.MemResponse.Advance(_readSegment.Length); } //Disposing of memory response can be deferred until the end of the request since its always syncrhonous } else { - if (count > 0) + /* + * When streams are used, callers will submit an explict length value + * which must be respected. This allows the stream size to differ from + * the actual content length. This is useful for when the stream is + * non-seekable, or does not have a known length + */ + + long total = 0; + while (total < Length) { - //Buffer is required, and count must be supplied + //get offset wrapper of the total buffer or remaining count + Memory offset = buffer[..(int)Math.Min(buffer.Length, Length - total)]; - long total = 0; - int read; - while (true) + //read + int read = await _userState.Stream!.ReadAsync(offset); + + //Guard + if (read == 0) { - //get offset wrapper of the total buffer or remaining count - Memory offset = buffer[..(int)Math.Min(buffer.Length, count - total)]; - //read - read = await _streamResponse!.ReadAsync(offset); - //Guard - if (read == 0) - { - break; - } - //write only the data that was read (slice) - await dest.WriteAsync(offset[..read]); - //Update total - total += read; + break; } - } - else - { - //Read in loop - do - { - //read - int read = await _streamResponse!.ReadAsync(buffer); - //Guard - if (read == 0) - { - break; - } - //write only the data that was read (slice) - await dest.WriteAsync(buffer[..read]); + //write only the data that was read (slice) + await dest.WriteAsync(offset[..read]); - } while (true); - } + //Update total + total += read; + } //Try to dispose the response stream asyncrhonously since we are done with it - await _streamResponse!.DisposeAsync(); - - //remove ref so its not disposed again - _streamResponse = null; + await _userState!.DisposeStreamAsync(); } } - ForwardOnlyMemoryReader _streamReader; - /// - async Task IHttpResponseBody.WriteEntityAsync(IResponseCompressor comp, IResponseDataWriter writer, Memory buffer) + public async Task WriteEntityAsync(IResponseCompressor comp, IResponseDataWriter writer, Memory buffer) { - //Locals - int read; - //Write a sliding window response - if (_memoryResponse != null) - { - while (_memoryResponse.Remaining > 0) + if (_userState.MemResponse != null) + { + while (_userState.MemResponse.Remaining > 0) { //Commit output bytes - if (CompressNextSegment(_memoryResponse, comp, writer)) + if (CompressNextSegment(_userState.MemResponse, comp, writer)) { //Time to flush await writer.FlushAsync(false); @@ -232,11 +183,14 @@ namespace VNLib.Net.Http.Core buffer = buffer[..comp.BlockSize]; } - //Process in loop - do + long total = 0; + while (total < Length) //If length was reached, break { + //get offset wrapper of the total buffer or remaining count + Memory offset = buffer[..(int)Math.Min(buffer.Length, Length - total)]; + //read - read = await _streamResponse!.ReadAsync(buffer, CancellationToken.None); + int read = await _userState.Stream!.ReadAsync(offset, CancellationToken.None); //Guard if (read == 0) @@ -244,9 +198,9 @@ namespace VNLib.Net.Http.Core break; } - //Track read bytes and loop uil all bytes are read - _streamReader = new(buffer[..read]); - + //Track read bytes and loop until all bytes are read + _streamReader = new(offset[..read]); + do { //Compress the buffered data and flush if required @@ -258,19 +212,17 @@ namespace VNLib.Net.Http.Core } while (_streamReader.WindowSize > 0); - } while (true); + //Update total + total += read; + } /* * Try to dispose the response stream asyncrhonously since we can safley here * otherwise it will be deferred until the end of the request cleanup */ - await _streamResponse!.DisposeAsync(); - - //remove ref so its not disposed again - _streamResponse = null; + await _userState.DisposeStreamAsync(); } - /* * Once there is no more response data avialable to compress * we need to flush the compressor, then flush the writer @@ -301,7 +253,7 @@ namespace VNLib.Net.Http.Core } while (true); } - + private static bool CompressNextSegment(IMemoryResponseReader reader, IResponseCompressor comp, IResponseDataWriter writer) { //Read the next segment @@ -338,17 +290,50 @@ namespace VNLib.Net.Http.Core [MethodImpl(MethodImplOptions.AggressiveInlining)] public void OnComplete() { - //Clear has data flag - HasData = false; - Length = 0; + //Clear rseponse containers + _userState.Dispose(); + _userState = default; + _readSegment = default; _streamReader = default; + } - //Clear rseponse containers - _streamResponse?.Dispose(); - _streamResponse = null; - _memoryResponse?.Close(); - _memoryResponse = null; + private readonly struct ResponsBodyDataState + { + public readonly long Legnth; + public readonly Stream? Stream; + public readonly IMemoryResponseReader? MemResponse; + public readonly bool IsSet; + + public ResponsBodyDataState(Stream stream, long length) + { + Legnth = length; + Stream = stream; + MemResponse = null; + IsSet = true; + } + + public ResponsBodyDataState(IMemoryResponseReader reader) + { + Legnth = reader.Remaining; + Stream = null; + MemResponse = reader; + IsSet = true; + } + + public readonly ValueTask DisposeStreamAsync() + { + return Stream?.DisposeAsync() ?? default; + } + + public readonly void Dispose() + { + if (IsSet) + { + Stream?.Dispose(); + MemResponse?.Close(); + } + } } } } \ No newline at end of file diff --git a/lib/Net.Http/src/Core/Response/ReusableResponseStream.cs b/lib/Net.Http/src/Core/Response/ReusableResponseStream.cs index 60465f9..3070c82 100644 --- a/lib/Net.Http/src/Core/Response/ReusableResponseStream.cs +++ b/lib/Net.Http/src/Core/Response/ReusableResponseStream.cs @@ -27,7 +27,7 @@ using System.IO; namespace VNLib.Net.Http.Core.Response { - internal abstract class ReusableResponseStream + internal abstract class ReusableResponseStream { protected Stream? transport; @@ -41,6 +41,6 @@ namespace VNLib.Net.Http.Core.Response /// Called when the connection is released /// public virtual void OnRelease() => transport = null; - + } } \ No newline at end of file diff --git a/lib/Net.Http/src/Core/ServerPreEncodedSegments.cs b/lib/Net.Http/src/Core/ServerPreEncodedSegments.cs deleted file mode 100644 index 4649813..0000000 --- a/lib/Net.Http/src/Core/ServerPreEncodedSegments.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Net.Http -* File: ServerPreEncodedSegments.cs -* -* ServerPreEncodedSegments.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 -{ - /// - /// Holds pre-encoded buffer segments for http request/responses - /// - /// - /// Holds ref to internal buffer - /// - internal readonly record struct ServerPreEncodedSegments(byte[] Buffer) - { - /// - /// Holds a pre-encoded segment for all crlf (line termination) bytes - /// - public readonly HttpEncodedSegment CrlfBytes { get; init; } = default; - - /// - /// Holds a pre-encoded segment for the final chunk termination - /// in http chuncked encoding - /// - public readonly HttpEncodedSegment FinalChunkTermination { get; init; } = default; - } -} \ No newline at end of file diff --git a/lib/Net.Http/src/Core/TransportReader.cs b/lib/Net.Http/src/Core/TransportReader.cs index a512331..7349387 100644 --- a/lib/Net.Http/src/Core/TransportReader.cs +++ b/lib/Net.Http/src/Core/TransportReader.cs @@ -26,11 +26,10 @@ using System; using System.IO; using System.Text; using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Runtime.CompilerServices; using VNLib.Utils; using VNLib.Utils.IO; +using VNLib.Utils.Memory; using VNLib.Net.Http.Core.Buffering; namespace VNLib.Net.Http.Core @@ -39,10 +38,8 @@ namespace VNLib.Net.Http.Core /// /// Structure implementation of /// - internal readonly struct TransportReader : IVnTextReader + internal struct TransportReader : IVnTextReader { - private readonly static int BufferPosStructSize = Unsafe.SizeOf(); - /// public readonly Encoding Encoding { get; } @@ -50,12 +47,13 @@ namespace VNLib.Net.Http.Core public readonly ReadOnlyMemory LineTermination { get; } /// - public readonly Stream BaseStream { get; } - + public readonly Stream BaseStream { get; } private readonly IHttpHeaderParseBuffer Buffer; private readonly uint MaxBufferSize; + private BufferPosition _position; + /// /// Initializes a new for reading text lines from the transport stream /// @@ -69,177 +67,97 @@ namespace VNLib.Net.Http.Core BaseStream = transport; LineTermination = lineTermination; Buffer = buffer; - MaxBufferSize = (uint)(buffer.BinSize - BufferPosStructSize); - - //Assign an zeroed position - BufferPosition position = default; - SetPosition(ref position); - - AssertZeroPosition(); - } - - [Conditional("DEBUG")] - private void AssertZeroPosition() - { - BufferPosition position = default; - GetPosition(ref position); - Debug.Assert(position.WindowStart == 0); - Debug.Assert(position.WindowEnd == 0); - } - - /// - /// Reads the current position from the buffer segment - /// - /// A reference to the varable to write the position to - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private readonly void GetPosition(ref BufferPosition position) - { - //Get the beining of the segment and read the position - Span span = Buffer.GetBinSpan(); - position = MemoryMarshal.Read(span); + MaxBufferSize = (uint)buffer.BinSize; + _position = default; } - /// - /// Updates the current position in the buffer segment - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private readonly void SetPosition(ref BufferPosition position) - { - //Store the position at the beining of the segment - Span span = Buffer.GetBinSpan(); - MemoryMarshal.Write(span, ref position); - } /// /// Gets the data segment of the buffer after the private segment /// /// - private readonly Span GetDataSegment() - { - //Get the beining of the segment - Span span = Buffer.GetBinSpan(); - //Return the segment after the private segment - return span[BufferPosStructSize..]; - } - + private readonly Span GetDataSegment() + => Buffer.GetBinSpan((int)_position.WindowStart, (int)_position.GetWindowSize()); + + private readonly Span GetRemainingSegment() => Buffer.GetBinSpan((int)_position.WindowEnd); + /// - public readonly int Available - { - get - { - //Read position and return the window size - BufferPosition position = default; - GetPosition(ref position); - return (int)position.GetWindowSize(); - } - } + public readonly int Available => (int)_position.GetWindowSize(); /// - public readonly Span BufferedDataWindow - { - get - { - //Read current position and return the window - BufferPosition position = default; - GetPosition(ref position); - return GetDataSegment()[(int)position.WindowStart..(int)position.WindowEnd]; - } - } + public readonly Span BufferedDataWindow => GetDataSegment(); /// - public readonly void Advance(int count) + public void Advance(int count) { if (count < 0) { throw new ArgumentOutOfRangeException(nameof(count), "Count must be positive"); } - //read the current position - BufferPosition position = default; - GetPosition(ref position); - //Advance the window start by the count and set the position - position.AdvanceStart(count); - SetPosition(ref position); + _position = _position.AdvanceStart(count); } /// - public readonly void FillBuffer() + public void FillBuffer() { - //Read the current position - BufferPosition bufferPosition = default; - GetPosition(ref bufferPosition); - - //Get a buffer from the end of the current window to the end of the buffer - Span bufferWindow = GetDataSegment()[(int)bufferPosition.WindowEnd..]; - - //Read from stream - int read = BaseStream.Read(bufferWindow); + //Read from stream into the remaining buffer segment + int read = BaseStream.Read(GetRemainingSegment()); Debug.Assert(read > -1, "Read should never be negative"); //Update the end of the buffer window to the end of the read data - bufferPosition.AdvanceEnd(read); - SetPosition(ref bufferPosition); + _position = _position.AdvanceEnd(read); } /// - public readonly ERRNO CompactBufferWindow() + public ERRNO CompactBufferWindow() { - //Read the current position - BufferPosition bufferPosition = default; - GetPosition(ref bufferPosition); + //store the current size of the window + uint windowSize = _position.GetWindowSize(); //No data to compact if window is not shifted away from start - if (bufferPosition.WindowStart > 0) + if (_position.WindowStart > 0) { - //Get span over engire buffer - Span buffer = GetDataSegment(); - - //Get used data segment within window - Span usedData = buffer[(int)bufferPosition.WindowStart..(int)bufferPosition.WindowEnd]; - - //Copy remaining to the begining of the buffer - usedData.CopyTo(buffer); - + //Get a ref to the entire buffer segment, then do an in-place move to shift the data to the start of the buffer + ref byte ptr = ref Buffer.DangerousGetBinRef(0); + MemoryUtil.Memmove(ref ptr, _position.WindowStart, ref ptr, 0, windowSize); + /* * Now that data has been shifted, update the position to * the new window and write the new position to the buffer */ - bufferPosition.Set(0, usedData.Length); - SetPosition(ref bufferPosition); + _position = BufferPosition.Set(0, windowSize); } //Return the number of bytes of available space from the end of the current window - return (nint)(MaxBufferSize - bufferPosition.WindowEnd); + return (nint)(MaxBufferSize - windowSize); } + - [StructLayout(LayoutKind.Sequential)] - private record struct BufferPosition + private readonly record struct BufferPosition { - public uint WindowStart; - public uint WindowEnd; - + public readonly uint WindowStart; + public readonly uint WindowEnd; + + private BufferPosition(uint start, uint end) + { + WindowStart = start; + WindowEnd = end; + } + /// /// Sets the the buffer window position /// /// Window start /// Window end - public void Set(int start, int end) - { - //Verify that the start and end are not negative - Debug.Assert(start >= 0, "Negative internal value passed to http buffer window start"); - Debug.Assert(end >= 0, "Negative internal value passed to http buffer window end"); - - WindowStart = (uint)start; - WindowEnd = (uint)end; - } + public static BufferPosition Set(uint start, uint end) => new(start, end); public readonly uint GetWindowSize() => WindowEnd - WindowStart; - public void AdvanceEnd(int count) => WindowEnd += (uint)count; + public readonly BufferPosition AdvanceEnd(int count) => new(WindowStart, WindowEnd + (uint)count); - public void AdvanceStart(int count) => WindowStart += (uint)count; + public readonly BufferPosition AdvanceStart(int count) => new(WindowStart + (uint)count, WindowEnd); } } } diff --git a/lib/Net.Http/src/Helpers/HttpControlMask.cs b/lib/Net.Http/src/Helpers/HttpControlMask.cs new file mode 100644 index 0000000..a2a004d --- /dev/null +++ b/lib/Net.Http/src/Helpers/HttpControlMask.cs @@ -0,0 +1,44 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpControlMask.cs +* +* HttpControlMask.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 +{ + /// + /// Contains HttpServer related function masks for altering http server + /// behavior + /// + public static class HttpControlMask + { + /// + /// Tells the http server that dynamic response compression should be disabled + /// + public const ulong CompressionDisabed = 0x01UL; + + /// + /// Tells the server not to set a 0 content length header when sending a response that does + /// not have an entity body to send. + /// + public const ulong ImplictContentLengthDisabled = 0x02UL; + } +} \ No newline at end of file diff --git a/lib/Net.Http/src/Helpers/HttpRange.cs b/lib/Net.Http/src/Helpers/HttpRange.cs new file mode 100644 index 0000000..cedcc40 --- /dev/null +++ b/lib/Net.Http/src/Helpers/HttpRange.cs @@ -0,0 +1,44 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpRange.cs +* +* HttpRange.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 +{ + /// + /// A structure that represents a range of bytes in an HTTP request + /// + /// The offset from the start of the resource + /// The ending byte range + /// Specifies the type of content range to observe + public readonly record struct HttpRange(ulong Start, ulong End, HttpRangeType RangeType) + { + /// + /// Gets a value indicating if the range is valid. A range is valid if + /// the start is less than or equal to the end. + /// + /// The starting range value + /// The ending range value + /// True if the range values are valid, false otherwise + public static bool IsValidRangeValue(ulong start, ulong end) => start <= end; + } +} \ No newline at end of file diff --git a/lib/Net.Http/src/Helpers/HttpRangeType.cs b/lib/Net.Http/src/Helpers/HttpRangeType.cs new file mode 100644 index 0000000..97d0721 --- /dev/null +++ b/lib/Net.Http/src/Helpers/HttpRangeType.cs @@ -0,0 +1,52 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpRangeType.cs +* +* HttpRangeType.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 +{ + /// + /// An enumeration of http range types to observe from http requests + /// + [Flags] + public enum HttpRangeType + { + /// + /// DO NOT USE, NOT VALID + /// + None = 0, + /// + /// A range of bytes from the start of the resource + /// + FromStart = 1, + /// + /// A range of bytes from the end of the resource + /// + FromEnd = 2, + /// + /// A full range of bytes from the start to the end of the resource + /// + FullRange = 3 + } +} \ No newline at end of file diff --git a/lib/Net.Http/src/HttpBufferConfig.cs b/lib/Net.Http/src/HttpBufferConfig.cs index 0191188..fa3ad21 100644 --- a/lib/Net.Http/src/HttpBufferConfig.cs +++ b/lib/Net.Http/src/HttpBufferConfig.cs @@ -70,7 +70,8 @@ namespace VNLib.Net.Http public readonly int ResponseBufferSize { get; init; } = 32 * 1024; /// - /// The size of the buffer used to accumulate chunked response data before writing to the transport + /// The size of the buffer used to accumulate chunked response data before writing to the transport. + /// May be set to 0 when is set to null (compression is disabled). /// public readonly int ChunkedResponseAccumulatorSize { get; init; } = 64 * 1024; } diff --git a/lib/Net.Http/src/HttpConfig.cs b/lib/Net.Http/src/HttpConfig.cs index e1bc103..274e163 100644 --- a/lib/Net.Http/src/HttpConfig.cs +++ b/lib/Net.Http/src/HttpConfig.cs @@ -44,19 +44,24 @@ namespace VNLib.Net.Http /// /// The absolute request entity body size limit in bytes /// - public readonly int MaxUploadSize { get; init; } = 5 * 1000 * 1024; + public readonly long MaxUploadSize { get; init; } = 5 * 1000 * 1024; /// /// The maximum size in bytes allowed for an MIME form-data content type upload /// /// Set to 0 to disabled mulit-part/form-data uploads - public readonly int MaxFormDataUploadSize { get; init; } = 40 * 1024; + public readonly int MaxFormDataUploadSize { get; init; } = 40 * 1024; + + /// + /// The maximum number of file uploads allowed per request + /// + public readonly ushort MaxUploadsPerRequest { get; init; } = 5; /// /// The maximum response entity size in bytes for which the library will allow compresssing response data /// /// Set this value to 0 to disable response compression - public readonly int CompressionLimit { get; init; } = 1000 * 1024; + public readonly long CompressionLimit { get; init; } = 1000 * 1024; /// /// The minimum size (in bytes) of respones data that will be compressed @@ -101,7 +106,7 @@ namespace VNLib.Net.Http /// will be returned and new connections closed. /// /// Set to 0 to disable request processing. Causes perminant 503 results - public readonly int MaxOpenConnections { get; init; } = int.MaxValue; + public readonly int MaxOpenConnections { get; init; } = int.MaxValue; /// /// An for writing verbose request logs. Set to null diff --git a/lib/Net.Http/src/IConnectionInfo.cs b/lib/Net.Http/src/IConnectionInfo.cs index 2549240..6cdb480 100644 --- a/lib/Net.Http/src/IConnectionInfo.cs +++ b/lib/Net.Http/src/IConnectionInfo.cs @@ -93,9 +93,10 @@ namespace VNLib.Net.Http Uri? Referer { get; } /// - /// The parsed range header, or -1,-1 if the range header was not set + /// The parsed range header, check the + /// to determine if the range has been set /// - Tuple? Range { get; } + HttpRange Range { get; } /// /// The server endpoint that accepted the connection diff --git a/lib/Net.Http/src/IHttpEvent.cs b/lib/Net.Http/src/IHttpEvent.cs index 8cd8f77..ce98db6 100644 --- a/lib/Net.Http/src/IHttpEvent.cs +++ b/lib/Net.Http/src/IHttpEvent.cs @@ -75,9 +75,12 @@ namespace VNLib.Net.Http /// Response status code /// MIME ContentType of data /// Data to be sent to client + /// Length of data to read from the stream and send to client /// + /// + /// /// - void CloseResponse(HttpStatusCode code, ContentType type, Stream stream); + void CloseResponse(HttpStatusCode code, ContentType type, Stream stream, long length); /// /// Responds to a client with an in-memory containing data @@ -86,6 +89,7 @@ namespace VNLib.Net.Http /// The status code to set /// The entity content-type /// The in-memory response data + /// /// void CloseResponse(HttpStatusCode code, ContentType type, IMemoryResponseReader entity); @@ -99,9 +103,9 @@ namespace VNLib.Net.Http void DangerousChangeProtocol(IAlternateProtocol protocolHandler); /// - /// Disables response compression + /// Sets an http server control mask to be applied to the current event flow /// - void DisableCompression(); + void SetControlFlag(ulong mask); } } \ No newline at end of file diff --git a/lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs b/lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs index 209ab91..c0321a4 100644 --- a/lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs +++ b/lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs @@ -26,6 +26,7 @@ using System; using System.IO; using System.Buffers; using System.Threading; +using System.Diagnostics; using System.Net.Sockets; using System.IO.Pipelines; using System.Threading.Tasks; @@ -102,6 +103,8 @@ namespace VNLib.Net.Transport.Tcp public bool Release() { + _sysSocketBufferSize = 0; + /* * If the pipeline has been started, then the pipes * will be completed by the worker threads (or by the streams) @@ -462,6 +465,8 @@ namespace VNLib.Net.Transport.Tcp private void CopyAndPublishDataOnSendPipe(ReadOnlyMemory src) { + Debug.Assert(_sysSocketBufferSize > 0, "A call to CopyAndPublishDataOnSendPipe was made before a socket was connected"); + /* * Clamp the buffer size to the system socket buffer size. If the * buffer is larger then, we will need to publish multiple segments diff --git a/lib/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs b/lib/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs index 447d17b..7c72bc4 100644 --- a/lib/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs +++ b/lib/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs @@ -375,10 +375,32 @@ namespace VNLib.Plugins.Essentials.Endpoints /// The status code to return to the client /// The response content type /// The stream response to return to the user + /// The explicit length of the stream + /// The operation result + public static VfReturnType VirtualClose(HttpEntity entity, HttpStatusCode code, ContentType ct, Stream response, long length) + { + entity.CloseResponse(code, ct, response, length); + return VfReturnType.VirtualSkip; + } + + /// + /// Shortcut helper methods to a virtual skip response with a given status code, + /// and memory content to return to the client + /// + /// The entity to close the connection for + /// The status code to return to the client + /// The response content type + /// The stream response to return to the user + /// The explicit length of the stream /// The operation result public static VfReturnType VirtualClose(HttpEntity entity, HttpStatusCode code, ContentType ct, Stream response) { - entity.CloseResponse(code, ct, response); + ArgumentNullException.ThrowIfNull(response, nameof(response)); + if (!response.CanSeek) + { + throw new IOException("The stream must be seekable when using implicit length"); + } + entity.CloseResponse(code, ct, response, response.Length); return VfReturnType.VirtualSkip; } diff --git a/lib/Plugins.Essentials/src/EventProcessor.cs b/lib/Plugins.Essentials/src/EventProcessor.cs index 3ea61c0..861f318 100644 --- a/lib/Plugins.Essentials/src/EventProcessor.cs +++ b/lib/Plugins.Essentials/src/EventProcessor.cs @@ -434,19 +434,84 @@ namespace VNLib.Plugins.Essentials //try to open the selected file for reading and allow sharing FileStream fs = new (filename, FileMode.Open, FileAccess.Read, FileShare.Read); - - //Check for range - if (entity.Server.Range != null && entity.Server.Range.Item1 > 0) - { - //Seek the stream to the specified position - fs.Seek(entity.Server.Range.Item1, SeekOrigin.Begin); - entity.CloseResponse(HttpStatusCode.PartialContent, fileType, fs); - } - else + + long endOffset = checked((long)entity.Server.Range.End); + long startOffset = checked((long)entity.Server.Range.Start); + + //Follows rfc7233 -> https://www.rfc-editor.org/rfc/rfc7233#section-1.2 + switch (entity.Server.Range.RangeType) { - //send the whole file - entity.CloseResponse(HttpStatusCode.OK, fileType, fs); + case HttpRangeType.FullRange: + if (endOffset > fs.Length || endOffset - startOffset < 0) + { + //Set acceptable range size + entity.Server.Headers[HttpResponseHeader.ContentRange] = $"bytes */{fs.Length}"; + + //The start offset is greater than the file length, return range not satisfiable + entity.CloseResponse(HttpStatusCode.RequestedRangeNotSatisfiable); + } + else + { + //Seek the stream to the specified start position + fs.Seek(startOffset, SeekOrigin.Begin); + + //Set range header, by passing the actual full content size + entity.SetContentRangeHeader(entity.Server.Range, fs.Length); + + //Send the response, with actual response length (diff between stream length and position) + entity.CloseResponse(HttpStatusCode.PartialContent, fileType, fs, endOffset - startOffset + 1); + } + break; + case HttpRangeType.FromStart: + if (startOffset > fs.Length) + { + //Set acceptable range size + entity.Server.Headers[HttpResponseHeader.ContentRange] = $"bytes */{fs.Length}"; + + //The start offset is greater than the file length, return range not satisfiable + entity.CloseResponse(HttpStatusCode.RequestedRangeNotSatisfiable); + } + else + { + //Seek the stream to the specified start position + fs.Seek(startOffset, SeekOrigin.Begin); + + //Set range header, by passing the actual full content size + entity.SetContentRangeHeader(entity.Server.Range, fs.Length); + + //Send the response, with actual response length (diff between stream length and position) + entity.CloseResponse(HttpStatusCode.PartialContent, fileType, fs, fs.Length - fs.Position); + } + break; + + case HttpRangeType.FromEnd: + if (endOffset > fs.Length) + { + //Set acceptable range size + entity.Server.Headers[HttpResponseHeader.ContentRange] = $"bytes */{fs.Length}"; + + //The end offset is greater than the file length, return range not satisfiable + entity.CloseResponse(HttpStatusCode.RequestedRangeNotSatisfiable); + } + else + { + //Seek the stream to the specified end position, server auto range will handle the rest + fs.Seek(-endOffset, SeekOrigin.End); + + //Set range header, by passing the actual full content size + entity.SetContentRangeHeader(entity.Server.Range, fs.Length); + + //Send the response, with actual response length (diff between stream length and position) + entity.CloseResponse(HttpStatusCode.PartialContent, fileType, fs, fs.Length - fs.Position); + } + break; + //No range or invalid range (the server is supposed to ignore invalid ranges) + default: + //send the whole file + entity.CloseResponse(HttpStatusCode.OK, fileType, fs, fs.Length); + break; } + } else { diff --git a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs index 5e0b04d..5c36465 100644 --- a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs +++ b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs @@ -30,6 +30,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using VNLib.Net.Http; +using VNLib.Utils.Memory; using VNLib.Utils.Extensions; namespace VNLib.Plugins.Essentials.Extensions @@ -154,6 +155,61 @@ namespace VNLib.Plugins.Essentials.Extensions server.Headers[HttpResponseHeader.LastModified] = value.ToString("R"); } + + /// + /// Sets the content-range header to the specified parameters + /// + /// + /// The http range used to return set the response header + /// The total content length + /// + public static void SetContentRangeHeader(this IHttpEvent entity, in HttpRange range, long length) + { + if(length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length), "Length must be greater than or equal to zero"); + } + + ulong start; + ulong end; + + //Determine start and end range from actual length and range + switch (range.RangeType) + { + case HttpRangeType.FullRange: + start = range.Start; + end = range.End; + break; + + case HttpRangeType.FromStart: + start = range.Start; + end = (ulong)length - 1; + break; + + case HttpRangeType.FromEnd: + start = (ulong)length - range.End; + end = (ulong)length - 1; + break; + + default: + throw new InvalidOperationException("Invalid range type"); + } + + + //Alloc enough space to hold the string + Span buffer = stackalloc char[64]; + ForwardOnlyWriter rangeBuilder = new(buffer); + //Build the range header in this format "bytes -/" + rangeBuilder.Append("bytes "); + rangeBuilder.Append(start); + rangeBuilder.Append('-'); + rangeBuilder.Append(end); + rangeBuilder.Append('/'); + rangeBuilder.Append(length); + //Print to a string and set the content range header + entity.Server.Headers[HttpResponseHeader.ContentRange] = rangeBuilder.ToString(); + } + /// /// Is the connection requesting cors /// diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs index 976eed5..b09924f 100644 --- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs +++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs @@ -250,12 +250,13 @@ namespace VNLib.Plugins.Essentials.Extensions /// The data to straem to the client as an attatcment /// The that represents the file /// The name of the file to attach + /// Explicit length of the stream data /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, ContentType ct, Stream data, string fileName) + public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, ContentType ct, Stream data, string fileName, long length) { //Close with file - ev.CloseResponse(code, ct, data); + ev.CloseResponse(code, ct, data, length); //Set content dispostion as attachment (only if successfull) ev.Server.Headers["Content-Disposition"] = $"attachment; filename=\"{fileName}\""; } @@ -304,9 +305,10 @@ namespace VNLib.Plugins.Essentials.Extensions //Get content type from filename ContentType ct = HttpHelpers.GetContentTypeFromFile(file.Name); //Set the input as a stream - ev.CloseResponse(code, ct, file); + ev.CloseResponse(code, ct, file, file.Length); } - + + /// /// Close a response to a connection with a character buffer using the server wide /// encoding @@ -321,7 +323,7 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data) { - //Get a memory stream using UTF8 encoding + //Get a memory stream using server built-in encoding CloseResponse(ev, code, type, data, ev.Server.Encoding); } @@ -344,7 +346,7 @@ namespace VNLib.Plugins.Essentials.Extensions } //Validate encoding - _ = encoding ?? throw new ArgumentNullException(nameof(encoding)); + ArgumentNullException.ThrowIfNull(encoding, nameof(encoding)); //Get new simple memory response IMemoryResponseReader reader = new SimpleMemoryResponse(data, encoding); @@ -790,6 +792,13 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ContentTypeString(this in FileUpload upload) => HttpHelpers.GetContentTypeString(upload.ContentType); + /// + /// Sets the flag on the current + /// instance to disable dynamic compression on the response. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void DisableCompression(this IHttpEvent entity) => entity.SetControlFlag(HttpControlMask.CompressionDisabed); /// /// Attempts to upgrade the connection to a websocket, if the setup fails, it sets up the response to the client accordingly. diff --git a/lib/Plugins.Essentials/src/HttpEntity.cs b/lib/Plugins.Essentials/src/HttpEntity.cs index 64f18ec..f48198b 100644 --- a/lib/Plugins.Essentials/src/HttpEntity.cs +++ b/lib/Plugins.Essentials/src/HttpEntity.cs @@ -183,14 +183,15 @@ namespace VNLib.Plugins.Essentials /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void CloseResponse(HttpStatusCode code, ContentType type, Stream stream) + public void CloseResponse(HttpStatusCode code, ContentType type, Stream stream, long length) { - Entity.CloseResponse(code, type, stream); //Verify content type matches if (!Server.Accepts(type)) { throw new ContentTypeUnacceptableException("The client does not accept the content type of the response"); } + + Entity.CloseResponse(code, type, stream, length); } /// @@ -209,7 +210,7 @@ namespace VNLib.Plugins.Essentials /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void DisableCompression() => Entity.DisableCompression(); + public void SetControlFlag(ulong mask) => Entity.SetControlFlag(mask); /* * Do not directly expose dangerous methods, but allow them to be called -- cgit