aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-12-10 16:26:41 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-12-10 16:26:41 -0500
commit546abea662263ef112c571c29706c47e875e09c4 (patch)
tree873c9176a4eb313b248fec9df700aa5ffe940954 /lib
parent01a654ff9a890be316734a42a207bf2dc14439c0 (diff)
http overhaul, fix range, remove range auto-processing, fix some perf bottlenecks from profiling
Diffstat (limited to 'lib')
-rw-r--r--lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs28
-rw-r--r--lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs77
-rw-r--r--lib/Net.Http/src/Core/Buffering/IHttpBuffer.cs21
-rw-r--r--lib/Net.Http/src/Core/Buffering/ISplitHttpBuffer.cs2
-rw-r--r--lib/Net.Http/src/Core/Buffering/SplitHttpBufferElement.cs21
-rw-r--r--lib/Net.Http/src/Core/ConnectionInfo.cs24
-rw-r--r--lib/Net.Http/src/Core/HttpContext.cs54
-rw-r--r--lib/Net.Http/src/Core/HttpEncodedSegment.cs71
-rw-r--r--lib/Net.Http/src/Core/HttpEvent.cs57
-rw-r--r--lib/Net.Http/src/Core/HttpServerBase.cs79
-rw-r--r--lib/Net.Http/src/Core/HttpServerProcessing.cs58
-rw-r--r--lib/Net.Http/src/Core/IHttpContextInformation.cs9
-rw-r--r--lib/Net.Http/src/Core/IHttpResponseBody.cs3
-rw-r--r--lib/Net.Http/src/Core/Request/HttpRequest.cs182
-rw-r--r--lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs93
-rw-r--r--lib/Net.Http/src/Core/Request/HttpRequestState.cs119
-rw-r--r--lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs169
-rw-r--r--lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs194
-rw-r--r--lib/Net.Http/src/Core/Response/ChunkedStream.cs97
-rw-r--r--lib/Net.Http/src/Core/Response/HeaderDataAccumulator.cs55
-rw-r--r--lib/Net.Http/src/Core/Response/HttpContextExtensions.cs50
-rw-r--r--lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs75
-rw-r--r--lib/Net.Http/src/Core/Response/HttpResponse.cs140
-rw-r--r--lib/Net.Http/src/Core/Response/IDirectResponsWriter.cs6
-rw-r--r--lib/Net.Http/src/Core/Response/ResponseWriter.cs247
-rw-r--r--lib/Net.Http/src/Core/Response/ReusableResponseStream.cs4
-rw-r--r--lib/Net.Http/src/Core/TransportReader.cs170
-rw-r--r--lib/Net.Http/src/Helpers/HttpControlMask.cs (renamed from lib/Net.Http/src/Core/ServerPreEncodedSegments.cs)26
-rw-r--r--lib/Net.Http/src/Helpers/HttpRange.cs44
-rw-r--r--lib/Net.Http/src/Helpers/HttpRangeType.cs (renamed from lib/Net.Http/src/Core/Response/DirectStream.cs)37
-rw-r--r--lib/Net.Http/src/HttpBufferConfig.cs3
-rw-r--r--lib/Net.Http/src/HttpConfig.cs13
-rw-r--r--lib/Net.Http/src/IConnectionInfo.cs5
-rw-r--r--lib/Net.Http/src/IHttpEvent.cs10
-rw-r--r--lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs5
-rw-r--r--lib/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs24
-rw-r--r--lib/Plugins.Essentials/src/EventProcessor.cs87
-rw-r--r--lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs56
-rw-r--r--lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs21
-rw-r--r--lib/Plugins.Essentials/src/HttpEntity.cs7
40 files changed, 1379 insertions, 1064 deletions
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<T>
+ readonly struct HttpBufferSegments<T>
{
public readonly Memory<T> HeaderAccumulator { get; init; }
public readonly Memory<T> 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<byte> 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<byte> 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<byte> buffer) => _handle = new(buffer);
+
+ ///<inheritdoc/>
+ public int Size => _handle.Size;
///<inheritdoc/>
- public int Size => _size;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public virtual Span<byte> GetBinSpan(int offset) => GetBinSpan(offset, Size - offset);
///<inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public virtual Span<byte> GetBinSpan() => MemoryUtil.GetSpan<byte>(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);
+ }
///<inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public virtual Memory<byte> GetMemory() => Buffer;
+ public virtual Memory<byte> GetMemory() => _handle.Memory;
+ ///<inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected virtual Span<byte> GetBinSpan(int maxSize)
+ public virtual Span<byte> 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<byte>(ref Pinned, maxSize);
+ private readonly MemoryHandle _handle;
+ private readonly IntPtr _pointer;
+
+ public readonly int Size;
+ public readonly Memory<byte> Memory;
+
+ public HandleState(Memory<byte> mem)
+ {
+ Memory = mem;
+ Size = mem.Length;
+ _handle = mem.Pin();
+ _pointer = MemoryUtil.GetIntptr(ref _handle);
+ }
+
+ public readonly void Unpin() => _handle.Dispose();
+
+ public readonly Span<byte> 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<byte>(IntPtr.Add(_pointer, offset), size);
+ }
+
+ public readonly ref byte GetRef() => ref MemoryUtil.GetRef<byte>(_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
/// <summary>
/// Gets the internal buffer as a span of bytes as fast as possible
/// </summary>
+ /// <param name="offset">The number of bytes to offset the start of the segment</param>
/// <returns>The memory block as a span</returns>
- Span<byte> GetBinSpan();
+ Span<byte> GetBinSpan(int offset);
+
+ /// <summary>
+ /// Gets the internal buffer as a span of bytes as fast as possible
+ /// with a specified offset and size
+ /// </summary>
+ /// <param name="offset">The number of bytes to offset the start of the segment</param>
+ /// <param name="size">The size of the desired segment</param>
+ /// <returns></returns>
+ Span<byte> GetBinSpan(int offset, int size);
+
+ /// <summary>
+ /// 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
+ /// </summary>
+ /// <param name="offset">The number of bytes to offset the returned reference to</param>
+ /// <returns>A reference to the first byte of the desired sequence</returns>
+ ref byte DangerousGetBinRef(int offset);
/// <summary>
/// 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
{
/// <summary>
- /// Gets the character segment of the internal buffer as a span of chars, which may be slower than <see cref="IHttpBuffer.GetBinSpan"/>
+ /// Gets the character segment of the internal buffer as a span of chars, which may be slower than <see cref="IHttpBuffer.GetBinSpan(int)"/>
/// but still considered a hot-path
/// </summary>
/// <returns>The character segment of the internal buffer</returns>
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
///<inheritdoc/>
public int BinSize { get; }
- internal SplitHttpBufferElement(int binSize)
- {
- BinSize = binSize;
- }
+ internal SplitHttpBufferElement(int binSize) => BinSize = binSize;
///<inheritdoc/>
public Span<char> GetCharSpan()
{
- //Get full buffer span
- Span<byte> _base = base.GetBinSpan();
-
- //Upshift to end of bin buffer
- _base = _base[BinSize..];
+ //Get space available after binary buffer
+ Span<byte> _base = base.GetBinSpan(BinSize);
//Return char span
return MemoryMarshal.Cast<byte, char>(_base);
@@ -59,8 +53,15 @@ namespace VNLib.Net.Http.Core.Buffering
*/
///<inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public override Span<byte> GetBinSpan() => base.GetBinSpan(BinSize);
+ public override Span<byte> GetBinSpan(int offset) => base.GetBinSpan(offset, BinSize);
+ /*
+ * Override to trim the bin buffer to the actual size of the
+ * binary segment of the buffer
+ */
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override Span<byte> GetBinSpan(int offset, int size) => base.GetBinSpan(offset, Math.Min(BinSize, size));
/// <summary>
/// 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;
///<inheritdoc/>
- public Uri RequestUri => Context.Request.Location;
+ public Uri RequestUri => Context.Request.State.Location;
///<inheritdoc/>
public string Path => RequestUri.LocalPath;
///<inheritdoc/>
- public string? UserAgent => Context.Request.UserAgent;
+ public string? UserAgent => Context.Request.State.UserAgent;
///<inheritdoc/>
public IHeaderCollection Headers { get; private set; }
@@ -55,28 +55,28 @@ namespace VNLib.Net.Http
public bool IsWebSocketRequest { get; }
///<inheritdoc/>
- public ContentType ContentType => Context.Request.ContentType;
+ public ContentType ContentType => Context.Request.State.ContentType;
///<inheritdoc/>
- public HttpMethod Method => Context.Request.Method;
+ public HttpMethod Method => Context.Request.State.Method;
///<inheritdoc/>
- public HttpVersion ProtocolVersion => Context.Request.HttpVersion;
+ public HttpVersion ProtocolVersion => Context.Request.State.HttpVersion;
///<inheritdoc/>
- public Uri? Origin => Context.Request.Origin;
+ public Uri? Origin => Context.Request.State.Origin;
///<inheritdoc/>
- public Uri? Referer => Context.Request.Referrer;
+ public Uri? Referer => Context.Request.State.Referrer;
///<inheritdoc/>
- public Tuple<long, long>? Range => Context.Request.Range;
+ public HttpRange Range => Context.Request.State.Range;
///<inheritdoc/>
- public IPEndPoint LocalEndpoint => Context.Request.LocalEndPoint;
+ public IPEndPoint LocalEndpoint => Context.Request.State.LocalEndPoint;
///<inheritdoc/>
- public IPEndPoint RemoteEndpoint => Context.Request.RemoteEndPoint;
+ public IPEndPoint RemoteEndpoint => Context.Request.State.RemoteEndPoint;
///<inheritdoc/>
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,18 +31,14 @@ 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
{
/// <summary>
- /// When set as a response flag, disables response compression for
- /// the current request/response flow
- /// </summary>
- public const ulong COMPRESSION_DISABLED_MSK = 0x01UL;
-
- /// <summary>
/// The reusable http request container
/// </summary>
public readonly HttpRequest Request;
@@ -60,7 +56,7 @@ namespace VNLib.Net.Http.Core
/// <summary>
/// The response entity body container
/// </summary>
- public readonly IHttpResponseBody ResponseBody;
+ public readonly ResponseWriter ResponseBody;
/// <summary>
/// A collection of flags that can be used to control the way the context
@@ -83,35 +79,35 @@ namespace VNLib.Net.Http.Core
/// </remarks>
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();
}
/// <summary>
@@ -126,10 +122,13 @@ namespace VNLib.Net.Http.Core
Encoding IHttpContextInformation.Encoding => ParentServer.Config.HttpEncoding;
///<inheritdoc/>
- HttpVersion IHttpContextInformation.CurrentVersion => Request.HttpVersion;
+ HttpVersion IHttpContextInformation.CurrentVersion => Request.State.HttpVersion;
+
+ ///<inheritdoc/>
+ public ref readonly HttpEncodedSegment CrlfSegment => ref ParentServer.CrlfBytes;
///<inheritdoc/>
- public ServerPreEncodedSegments EncodedSegments => ParentServer.PreEncodedSegments;
+ public ref readonly HttpEncodedSegment FinalChunkSegment => ref ParentServer.FinalChunkBytes;
///<inheritdoc/>
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();
}
///<inheritdoc/>
@@ -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
/// <param name="Buffer">The buffer containing the segment data</param>
/// <param name="Offset">The offset in the buffer to begin the segment at</param>
/// <param name="Length">The length of the segment</param>
- internal readonly record struct HttpEncodedSegment(byte[] Buffer, int Offset, int Length)
+ internal readonly record struct HttpEncodedSegment(byte[] Buffer, uint Offset, uint Length)
{
/// <summary>
- /// Span representation of the pre-encoded segment
+ /// Validates the bounds of the array so calls to <see cref="DangerousCopyTo(Span{byte})"/>
+ /// won't cause a buffer over/under run
+ /// </summary>
+ public readonly void Validate() => MemoryUtil.CheckBounds(Buffer, Offset, Length);
+
+ /// <summary>
+ /// Performs a dangerous reference based copy-to (aka memmove)
+ /// </summary>
+ /// <param name="output">The output buffer to write the encoded segment to</param>
+ internal readonly int DangerousCopyTo(Span<byte> 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));
+ }
+
+ /// <summary>
+ /// Performs a dangerous reference based copy-to (aka memmove)
+ /// to the supplied <see cref="IHttpBuffer"/> at the supplied offset.
+ /// This operation performs bounds checks
/// </summary>
- public Span<byte> Span => Buffer.AsSpan(Offset, Length);
+ /// <param name="buffer">The <see cref="IHttpBuffer"/> to copy data to</param>
+ /// <param name="offset">The byte offset to the first byte of the desired segment</param>
+ /// <returns>The number of bytes written to the segment</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ 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;
+ }
/// <summary>
- /// Memory representation of the pre-encoded segment
+ /// Allocates a new <see cref="HttpEncodedSegment"/> buffer from the supplied string
+ /// using the supplied encoding
/// </summary>
- public Memory<byte> Memory => Buffer.AsMemory(Offset, Length);
+ /// <param name="data">The string data to encode</param>
+ /// <param name="enc">The encoder used to convert the character data to bytes</param>
+ /// <returns>The initalized <see cref="HttpEncodedSegment"/> structure</returns>
+ 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();
+ }
///<inheritdoc/>
IConnectionInfo IHttpEvent.Server => _ci;
@@ -50,14 +53,16 @@ namespace VNLib.Net.Http
HttpServer IHttpEvent.OriginServer => Context.ParentServer;
///<inheritdoc/>
- IReadOnlyDictionary<string, string> IHttpEvent.QueryArgs => Context.Request.RequestBody.QueryArgs;
+ IReadOnlyDictionary<string, string> IHttpEvent.QueryArgs => Context.Request.QueryArgs;
+
///<inheritdoc/>
- IReadOnlyDictionary<string, string> IHttpEvent.RequestArgs => Context.Request.RequestBody.RequestArgs;
+ IReadOnlyDictionary<string, string> IHttpEvent.RequestArgs => Context.Request.RequestArgs;
+
///<inheritdoc/>
- IReadOnlyList<FileUpload> IHttpEvent.Files => Context.Request.RequestBody.Uploads;
+ IReadOnlyList<FileUpload> IHttpEvent.Files => _uploads;
///<inheritdoc/>
- void IHttpEvent.DisableCompression() => Context.ContextFlags.Set(HttpContext.COMPRESSION_DISABLED_MSK);
+ void IHttpEvent.SetControlFlag(ulong mask) => Context.ContextFlags.Set(mask);
///<inheritdoc/>
void IHttpEvent.DangerousChangeProtocol(IAlternateProtocol protocolHandler)
@@ -79,16 +84,23 @@ namespace VNLib.Net.Http
void IHttpEvent.CloseResponse(HttpStatusCode code) => Context.Respond(code);
///<inheritdoc/>
- 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));
+ }
}
///<inheritdoc/>
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
/// </summary>
private readonly string KeepAliveTimeoutHeaderValue;
+
/// <summary>
/// Reusable store for obtaining <see cref="HttpContext"/>
/// </summary>
private readonly ObjectRental<HttpContext> ContextStore;
+
/// <summary>
/// The cached header-line termination value
/// </summary>
@@ -107,14 +108,20 @@ namespace VNLib.Net.Http
public bool Running { get; private set; }
/// <summary>
- /// The <see cref="ServerPreEncodedSegments"/> for the current server
- /// </summary>
- internal readonly ServerPreEncodedSegments PreEncodedSegments;
- /// <summary>
/// Cached supported compression methods
/// </summary>
internal readonly CompressionMethod SupportedCompressionMethods;
+ /// <summary>
+ /// Pre-encoded CRLF bytes
+ /// </summary>
+ internal readonly HttpEncodedSegment CrlfBytes;
+
+ /// <summary>
+ /// Pre-encoded HTTP chunking final chunk segment
+ /// </summary>
+ internal readonly HttpEncodedSegment FinalChunkBytes;
+
private CancellationTokenSource? StopToken;
/// <summary>
@@ -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<byte> 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
- };
- }
-
/// <summary>
/// Begins listening for connections on configured interfaces for configured hostnames.
/// </summary>
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<byte>.Empty, StopToken!.Token);
+ await stream.ReadAsync(Memory<byte>.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
{
/// <summary>
- /// Gets pre-encoded binary segments for the current server's encoding
+ /// Gets a reference to the <see cref="HttpEncodedSegment"/> containing the CRLF sequence
/// </summary>
- ServerPreEncodedSegments EncodedSegments { get; }
+ ref readonly HttpEncodedSegment CrlfSegment { get; }
+
+ /// <summary>
+ /// Gets a reference to the <see cref="HttpEncodedSegment"/> containing the final chunk sequence
+ /// </summary>
+ ref readonly HttpEncodedSegment FinalChunkSegment { get; }
/// <summary>
/// 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
/// </summary>
/// <param name="dest">The response stream to write data to</param>
/// <param name="buffer">An optional buffer used to buffer responses</param>
- /// <param name="count">The maximum length of the response data to write</param>
/// <returns>A task that resolves when the response is completed</returns>
- Task WriteEntityAsync(IDirectResponsWriter dest, long count, Memory<byte> buffer);
+ Task WriteEntityAsync(IDirectResponsWriter dest, Memory<byte> buffer);
/// <summary>
/// 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<string, string> Cookies;
public readonly List<string> Accept;
public readonly List<string> 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<long, long>? Range { get; set; }
+ public readonly Dictionary<string, string> Cookies;
+ public readonly Dictionary<string, string> RequestArgs;
+ public readonly Dictionary<string, string> QueryArgs;
/// <summary>
- /// 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
/// </summary>
- 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;
/// <summary>
- /// 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.
/// </summary>
- public HttpInputStream InputStream { get; }
+ /// <returns>A mutable reference to the state structure for initalization purposes</returns>
+ internal ref HttpRequestState GetMutableStateForInit() => ref _state;
/// <summary>
- /// A value indicating if the client's request had an Expect-100-Continue header
+ /// A readonly reference to the internal request state once initialized
/// </summary>
- 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()
+ { }
+ /// <summary>
+ /// Initializes the <see cref="HttpRequest"/> for an incomming connection
+ /// </summary>
+ /// <param name="ctx">The <see cref="ITransportContext"/> to attach the request to</param>
+ /// <param name="defaultHttpVersion">The default http version</param>
[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;
+ }
+ }
+
+ /// <summary>
+ /// Checks if another upload can be added to the request
+ /// </summary>
+ /// <returns>A value indicating if another file upload can be added to the array</returns>
+ public bool CanAddUpload() => _state.UploadCount < _uploads.Length;
+
+ /// <summary>
+ /// Creates a new array and copies the uploads to it.
+ /// </summary>
+ /// <returns>The array clone of the file uploads</returns>
+ public FileUpload[] CopyUploads()
+ {
+ if (_state.UploadCount == 0)
+ {
+ return Array.Empty<FileUpload>();
+ }
+ //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<char> 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));
}
/// <summary>
@@ -117,22 +117,6 @@ namespace VNLib.Net.Http.Core
}
/// <summary>
- /// Initializes the <see cref="HttpRequest"/> for an incomming connection
- /// </summary>
- /// <param name="server"></param>
- /// <param name="ctx">The <see cref="ITransportContext"/> to attach the request to</param>
- /// <param name="defaultHttpVersion">The default http version</param>
- [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;
- }
-
-
- /// <summary>
/// Initializes the <see cref="HttpRequest.RequestBody"/> for the current request
/// </summary>
/// <param name="context"></param>
@@ -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<byte> 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<char>)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<char> 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<char> 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<char> fileData = reader.Window.TrimCRLF();
+ //Only add the upload if the request can accept more uploads, otherwise drop it
+ if (state.Request.CanAddUpload())
+ {
+ ReadOnlySpan<char> 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<char> 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<char> 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<char> queryString = Request.Location.Query;
+ ReadOnlySpan<char> 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
+{
+
+ /// <summary>
+ /// A mutable http connection state structure that stores HTTP
+ /// status information
+ /// </summary>
+ internal struct HttpRequestState
+ {
+ /// <summary>
+ /// A value indicating if the client's request had an Expect-100-Continue header
+ /// </summary>
+ internal bool Expect;
+
+ /// <summary>
+ /// A value that indicates if HTTP keepalive is desired by a client and is respected
+ /// by the server
+ /// </summary>
+ internal bool KeepAlive;
+
+ /// <summary>
+ /// A value indicating whether the connection contained a request entity body.
+ /// </summary>
+ internal bool HasEntityBody;
+
+ /// <summary>
+ /// The connection HTTP version determined by the server.
+ /// </summary>
+ public HttpVersion HttpVersion;
+
+ /// <summary>
+ /// The requested HTTP method
+ /// </summary>
+ public HttpMethod Method;
+
+ /// <summary>
+ /// Request wide content type of a request entity body if not using FormData
+ /// </summary>
+ public ContentType ContentType;
+
+ /// <summary>
+ /// Conent range requested ranges, that are parsed into a start-end tuple
+ /// </summary>
+ public HttpRange Range;
+
+ /// <summary>
+ /// The number of uploaded files in the request
+ /// </summary>
+ public int UploadCount;
+
+ /// <summary>
+ /// request's user-agent string
+ /// </summary>
+ public string? UserAgent;
+
+ /// <summary>
+ /// Boundry header value if reuqest send data using MIME mulit-part form data
+ /// </summary>
+ public string? Boundry;
+
+ /// <summary>
+ /// Request entity body charset if parsed during content-type parsing
+ /// </summary>
+ public string? Charset;
+
+ /// <summary>
+ /// The requested resource location url
+ /// </summary>
+ public Uri Location;
+
+ /// <summary>
+ /// The value of the origin header if one was sent
+ /// </summary>
+ public Uri? Origin;
+
+ /// <summary>
+ /// The url value of the referer header if one was sent
+ /// </summary>
+ public Uri? Referrer;
+
+ /// <summary>
+ /// The connection's remote endpoint (ip/port) captured from transport
+ /// </summary>
+ public IPEndPoint RemoteEndPoint;
+
+ /// <summary>
+ /// The connection's local endpoint (the server's transport socket address)
+ /// </summary>
+ 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<char> 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<char> 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<char> 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<char> 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<char> 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<char> port = requestHeaderValue.SliceAfterParam(':').Trim();
- //Slicing beofre the colon should always provide a useable hostname, so allocate a string for it
- string host = requestHeaderValue.SliceBeforeParam(':').Trim().ToString();
+ //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<char> 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<char> startRange = rawRange.SliceBeforeParam('-');
-
//Get end range (empty if no - exists)
ReadOnlySpan<char> endRange = rawRange.SliceAfterParam('-');
- //See if a range end is specified
- if (endRange.IsEmpty)
+ //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
{
+
/// <summary>
/// A specialized <see cref="IDataAccumulator{T}"/> for buffering data
/// in Http/1.1 chunks
/// </summary>
- internal class ChunkDataAccumulator : IDataAccumulator<byte>
+ 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;
-
- ///<inheritdoc/>
- public int RemainingSize => Buffer.Size - AccumulatedSize;
-
- ///<inheritdoc/>
- Span<byte> IDataAccumulator<byte>.Remaining => Buffer.GetBinSpan()[AccumulatedSize..];
-
- ///<inheritdoc/>
- Span<byte> IDataAccumulator<byte>.Accumulated => Buffer.GetBinSpan()[_reservedOffset.. AccumulatedSize];
-
- ///<inheritdoc/>
- public int AccumulatedSize { get; set; }
-
- ///<inheritdoc/>
- public void Advance(int count) => AccumulatedSize += count;
-
- /// <summary>
- /// Gets the remaining segment of the buffer to write chunk data to.
- /// </summary>
- /// <returns>The chunk buffer to write data to</returns>
- public Memory<byte> 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);
- }
-
- /// <summary>
- /// Calculates the usable remaining size of the chunk buffer.
- /// </summary>
- /// <returns>The number of bytes remaining in the buffer</returns>
- public int GetRemainingSegmentSize()
- {
- //Remaining size accounting for the trailing crlf
- return RemainingSize - Context.EncodedSegments.CrlfBytes.Length;
- }
/// <summary>
/// Complets and returns the memory segment containing the chunk data to send
/// to the client. This also resets the accumulator.
/// </summary>
/// <returns></returns>
- public Memory<byte> GetChunkData()
+ public readonly Memory<byte> 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];
}
/// <summary>
@@ -118,47 +83,59 @@ namespace VNLib.Net.Http.Core.Response
/// to the client.
/// </summary>
/// <returns></returns>
- public Memory<byte> GetFinalChunkData()
+ public readonly Memory<byte> 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];
}
+ /// <summary>
+ /// Gets the remaining segment of the buffer to write chunk data to.
+ /// </summary>
+ /// <returns>The chunk buffer to write data to</returns>
+ public readonly Memory<byte> GetRemainingSegment(int accumulatedSize)
+ {
+ int endOfDataOffset = GetPointerToEndOfUsedBuffer(accumulatedSize);
+ return Buffer.GetMemory()[endOfDataOffset..TotalMaxBufferSize];
+ }
+
+ /// <summary>
+ /// Calculates the usable remaining size of the chunk buffer.
+ /// </summary>
+ /// <returns>The number of bytes remaining in the buffer</returns>
+ 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<byte> GetCompleteChunk() => Buffer.GetMemory()[_reservedOffset..AccumulatedSize];
-
-
- private void InitReserved()
+ private readonly Memory<byte> GetCompleteChunk(int reservedOffset, int accumulatedSize)
{
- //First reserve the chunk window by advancing the accumulator to the reserved size
- Advance(ReservedSize);
- }
-
- ///<inheritdoc/>
- 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<char> s = stackalloc char[CharBufSize];
-
- //Chunk size is the accumulated size without the reserved segment
- int chunkSize = (AccumulatedSize - ReservedSize);
+ Span<char> intFormatBuffer = stackalloc char[CharBufSize];
+
+
+ //temp buffer to store binary encoded data in
+ Span<byte> 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<byte> 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<byte> 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<byte> 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
-{
-
- /// <summary>
- /// Writes chunked HTTP message bodies to an underlying streamwriter
- /// </summary>
- internal sealed class ChunkedStream : ReusableResponseStream, IResponseDataWriter
- {
- private readonly ChunkDataAccumulator ChunckAccumulator;
-
- internal ChunkedStream(IChunkAccumulatorBuffer buffer, IHttpContextInformation context)
- {
- //Init accumulator
- ChunckAccumulator = new(buffer, context);
- }
-
- #region Hooks
-
- ///<inheritdoc/>
- public void OnNewRequest()
- {
- ChunckAccumulator.OnNewRequest();
- }
-
- ///<inheritdoc/>
- public void OnComplete()
- {
- ChunckAccumulator.OnComplete();
- }
-
- ///<inheritdoc/>
- public Memory<byte> GetMemory() => ChunckAccumulator.GetRemainingSegment();
-
- ///<inheritdoc/>
- public int Advance(int written)
- {
- //Advance the accumulator
- ChunckAccumulator.Advance(written);
- return ChunckAccumulator.GetRemainingSegmentSize();
- }
-
- ///<inheritdoc/>
- 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<byte> 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/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
/// <summary>
/// Specialized data accumulator for compiling response headers
/// </summary>
- 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)
{
@@ -47,10 +47,16 @@ namespace VNLib.Net.Http.Core.Response
}
/// <summary>
+ /// Gets the accumulated response data as its memory buffer, and resets the internal accumulator
+ /// </summary>
+ /// <returns>The buffer segment containing the accumulated response data</returns>
+ public readonly Memory<byte> GetResponseData(int accumulatedSize) => _buffer.GetMemory()[..accumulatedSize];
+
+ /// <summary>
/// Initializes a new <see cref="ForwardOnlyWriter{T}"/> for buffering character header data
/// </summary>
/// <returns>A <see cref="ForwardOnlyWriter{T}"/> for buffering character header data</returns>
- public ForwardOnlyWriter<char> GetWriter()
+ public readonly ForwardOnlyWriter<char> GetWriter()
{
Span<char> chars = _buffer.GetCharSpan();
return new ForwardOnlyWriter<char>(chars);
@@ -60,7 +66,8 @@ namespace VNLib.Net.Http.Core.Response
/// Encodes and writes the contents of the <see cref="ForwardOnlyWriter{T}"/> to the internal accumulator
/// </summary>
/// <param name="writer">The character buffer writer to commit data from</param>
- public void CommitChars(ref ForwardOnlyWriter<char> writer)
+ /// <param name="accumulatedSize">A reference to the cumulative number of bytes written to the buffer</param>
+ public readonly void CommitChars(ref ForwardOnlyWriter<char> 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);
}
/// <summary>
/// Encodes a single token and writes it directly to the internal accumulator
/// </summary>
/// <param name="chars">The character sequence to accumulate</param>
- public void WriteToken(ReadOnlySpan<char> chars)
+ /// <param name="accumulatedSize">A reference to the cumulative number of bytes written to the buffer</param>
+ public readonly void WriteToken(ReadOnlySpan<char> chars, ref int accumulatedSize)
{
//Get remaining buffer
- Span<byte> remaining = _buffer.GetBinSpan()[AccumulatedSize..];
+ Span<byte> remaining = _buffer.GetBinSpan(accumulatedSize);
//Commit all chars to the buffer
- AccumulatedSize += _contextInfo.Encoding.GetBytes(chars, remaining);
+ accumulatedSize += _contextInfo.Encoding.GetBytes(chars, remaining);
}
/// <summary>
/// Writes the http termination sequence to the internal accumulator
/// </summary>
- public void WriteTermination()
- {
- //Write the http termination sequence
- Span<byte> 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;
- }
-
- /// <summary>
- /// Resets the internal accumulator
- /// </summary>
- public void Reset() => AccumulatedSize = 0;
-
- /// <summary>
- /// Gets the accumulated response data as its memory buffer, and resets the internal accumulator
- /// </summary>
- /// <returns>The buffer segment containing the accumulated response data</returns>
- public Memory<byte> GetResponseData()
- {
- //get the current buffer as memory and return the accumulated segment
- Memory<byte> 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
{
/// <summary>
/// Provides extended funcionality of an <see cref="HttpContext"/>
@@ -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);
-
+
/// <summary>
/// Sets CacheControl and Pragma headers to no-cache
/// </summary>
@@ -70,29 +68,6 @@ namespace VNLib.Net.Http.Core
Response.Headers[HttpResponseHeader.CacheControl] = CACHE_CONTROL_VALUE;
}
- /// <summary>
- /// Sets the content-range header to the specified parameters
- /// </summary>
- /// <param name="Response"></param>
- /// <param name="start">The content range start</param>
- /// <param name="end">The content range end</param>
- /// <param name="length">The total content length</param>
- public static void SetContentRange(this HttpResponse Response, long start, long end, long length)
- {
- //Alloc enough space to hold the string
- Span<char> buffer = stackalloc char[64];
- ForwardOnlyWriter<char> rangeBuilder = new(buffer);
- //Build the range header in this format "bytes <begin>-<end>/<total>"
- rangeBuilder.Append("bytes ");
- rangeBuilder.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<byte> GetRemainingConstrained(this IMemoryResponseReader reader, int limit)
{
@@ -101,24 +76,5 @@ namespace VNLib.Net.Http.Core
//get segment and slice
return reader.GetMemory()[..size];
}
-
- /// <summary>
- /// If an end-range is set, returns the remaining bytes up to the end-range, otherwise returns the entire request body length
- /// </summary>
- /// <param name="body"></param>
- /// <param name="range">The data range</param>
- /// <returns></returns>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static long GetResponseLengthWithRange(this IHttpResponseBody body, Tuple<long, long> 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<long, long> 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<byte> buffer = ResponseBody.BufferRequired ? Buffers.GetResponseDataBuffer() : Memory<byte>.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<HttpCookie> 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;
/// <summary>
@@ -62,7 +62,12 @@ namespace VNLib.Net.Http.Core
/// </summary>
public VnWebHeaderCollection Headers { get; }
- public HttpResponse(IHttpBufferManager manager, IHttpContextInformation ctx)
+ /// <summary>
+ /// The current http status code value
+ /// </summary>
+ 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<byte> responseBlock = Writer.GetResponseData();
+ Memory<byte> 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;
}
/// <summary>
@@ -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<byte> responseBlock = Writer.GetResponseData();
+ Memory<byte> 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;
}
///<inheritdoc/>
@@ -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
+ {
+ ///<inheritdoc/>
+ public ValueTask WriteAsync(ReadOnlyMemory<byte> buffer) => transport!.WriteAsync(buffer);
+ }
+
+ /// <summary>
+ /// Writes chunked HTTP message bodies to an underlying streamwriter
+ /// </summary>
+ 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
+
+ ///<inheritdoc/>
+ public void OnComplete() => _accumulatedBytes = 0;
+
+ ///<inheritdoc/>
+ public Memory<byte> GetMemory() => _chunkAccumulator.GetRemainingSegment(_accumulatedBytes);
+
+ ///<inheritdoc/>
+ public int Advance(int written)
+ {
+ //Advance the accumulator
+ _accumulatedBytes += written;
+ return _chunkAccumulator.GetRemainingSegmentSize(_accumulatedBytes);
+ }
+
+ ///<inheritdoc/>
+ 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<byte> 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<char> buffer = MemoryUtil.SafeAlloc<char>(16 * 1024);
//Writer
- ForwardOnlyWriter<char> writer = new (buffer.Span);
+ ForwardOnlyWriter<char> 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
/// <param name="buffer">The response data to write</param>
/// <returns>A value task that resolves when the write operation is complete</returns>
ValueTask WriteAsync(ReadOnlyMemory<byte> buffer);
-
- /// <summary>
- /// Flushes any remaining data to the client
- /// </summary>
- /// <returns>A task that resolves when the flush operationis complete</returns>
- 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;
+
///<inheritdoc/>
- public bool HasData { get; private set; }
+ public bool HasData => _userState.IsSet;
//Buffering is required when a stream is set
- bool IHttpResponseBody.BufferRequired => _streamResponse != null;
+ ///<inheritdoc/>
+ public bool BufferRequired => _userState.Stream != null;
///<inheritdoc/>
- public long Length { get; private set; }
+ public long Length => _userState.Legnth;
/// <summary>
/// Attempts to set the response body as a stream
/// </summary>
/// <param name="response">The stream response body to read</param>
+ /// <param name="length">Explicit length of the stream</param>
/// <returns>True if the response entity could be set, false if it has already been set</returns>
- 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
/// <returns>True if the response entity could be set, false if it has already been set</returns>
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<byte> _readSegment;
+ private ForwardOnlyMemoryReader<byte> _streamReader;
- ReadOnlyMemory<byte> _readSegment;
+#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
///<inheritdoc/>
- async Task IHttpResponseBody.WriteEntityAsync(IDirectResponsWriter dest, long count, Memory<byte> buffer)
+ public async Task WriteEntityAsync(IDirectResponsWriter dest, Memory<byte> 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<byte> 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<byte> 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<byte> _streamReader;
-
///<inheritdoc/>
- async Task IHttpResponseBody.WriteEntityAsync(IResponseCompressor comp, IResponseDataWriter writer, Memory<byte> buffer)
+ public async Task WriteEntityAsync(IResponseCompressor comp, IResponseDataWriter writer, Memory<byte> 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<byte> 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
/// </summary>
public virtual void OnRelease() => transport = null;
-
+
}
} \ 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
/// <summary>
/// Structure implementation of <see cref="IVnTextReader"/>
/// </summary>
- internal readonly struct TransportReader : IVnTextReader
+ internal struct TransportReader : IVnTextReader
{
- private readonly static int BufferPosStructSize = Unsafe.SizeOf<BufferPosition>();
-
///<inheritdoc/>
public readonly Encoding Encoding { get; }
@@ -50,12 +47,13 @@ namespace VNLib.Net.Http.Core
public readonly ReadOnlyMemory<byte> LineTermination { get; }
///<inheritdoc/>
- public readonly Stream BaseStream { get; }
-
+ public readonly Stream BaseStream { get; }
private readonly IHttpHeaderParseBuffer Buffer;
private readonly uint MaxBufferSize;
+ private BufferPosition _position;
+
/// <summary>
/// Initializes a new <see cref="TransportReader"/> for reading text lines from the transport stream
/// </summary>
@@ -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);
- }
-
- /// <summary>
- /// Reads the current position from the buffer segment
- /// </summary>
- /// <param name="position">A reference to the varable to write the position to</param>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private readonly void GetPosition(ref BufferPosition position)
- {
- //Get the beining of the segment and read the position
- Span<byte> span = Buffer.GetBinSpan();
- position = MemoryMarshal.Read<BufferPosition>(span);
+ MaxBufferSize = (uint)buffer.BinSize;
+ _position = default;
}
- /// <summary>
- /// Updates the current position in the buffer segment
- /// </summary>
- /// <param name="position"></param>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private readonly void SetPosition(ref BufferPosition position)
- {
- //Store the position at the beining of the segment
- Span<byte> span = Buffer.GetBinSpan();
- MemoryMarshal.Write(span, ref position);
- }
/// <summary>
/// Gets the data segment of the buffer after the private segment
/// </summary>
/// <returns></returns>
- private readonly Span<byte> GetDataSegment()
- {
- //Get the beining of the segment
- Span<byte> span = Buffer.GetBinSpan();
- //Return the segment after the private segment
- return span[BufferPosStructSize..];
- }
-
+ private readonly Span<byte> GetDataSegment()
+ => Buffer.GetBinSpan((int)_position.WindowStart, (int)_position.GetWindowSize());
+
+ private readonly Span<byte> GetRemainingSegment() => Buffer.GetBinSpan((int)_position.WindowEnd);
+
///<inheritdoc/>
- 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();
///<inheritdoc/>
- public readonly Span<byte> 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<byte> BufferedDataWindow => GetDataSegment();
///<inheritdoc/>
- 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);
}
///<inheritdoc/>
- 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<byte> 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);
}
///<inheritdoc/>
- 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<byte> buffer = GetDataSegment();
-
- //Get used data segment within window
- Span<byte> 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;
+ }
+
/// <summary>
/// Sets the the buffer window position
/// </summary>
/// <param name="start">Window start</param>
/// <param name="end">Window end</param>
- 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/Core/ServerPreEncodedSegments.cs b/lib/Net.Http/src/Helpers/HttpControlMask.cs
index 4649813..a2a004d 100644
--- a/lib/Net.Http/src/Core/ServerPreEncodedSegments.cs
+++ b/lib/Net.Http/src/Helpers/HttpControlMask.cs
@@ -3,10 +3,10 @@
*
* Library: VNLib
* Package: VNLib.Net.Http
-* File: ServerPreEncodedSegments.cs
+* File: HttpControlMask.cs
*
-* ServerPreEncodedSegments.cs is part of VNLib.Net.Http which
-* is part of the larger VNLib collection of libraries and utilities.
+* 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
@@ -22,25 +22,23 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
-namespace VNLib.Net.Http.Core
+namespace VNLib.Net.Http
{
/// <summary>
- /// Holds pre-encoded buffer segments for http request/responses
+ /// Contains HttpServer related function masks for altering http server
+ /// behavior
/// </summary>
- /// <param name="Buffer">
- /// Holds ref to internal buffer
- /// </param>
- internal readonly record struct ServerPreEncodedSegments(byte[] Buffer)
+ public static class HttpControlMask
{
/// <summary>
- /// Holds a pre-encoded segment for all crlf (line termination) bytes
+ /// Tells the http server that dynamic response compression should be disabled
/// </summary>
- public readonly HttpEncodedSegment CrlfBytes { get; init; } = default;
+ public const ulong CompressionDisabed = 0x01UL;
/// <summary>
- /// Holds a pre-encoded segment for the final chunk termination
- /// in http chuncked encoding
+ /// Tells the server not to set a 0 content length header when sending a response that does
+ /// not have an entity body to send.
/// </summary>
- public readonly HttpEncodedSegment FinalChunkTermination { get; init; } = default;
+ 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
+{
+ /// <summary>
+ /// A structure that represents a range of bytes in an HTTP request
+ /// </summary>
+ /// <param name="Start">The offset from the start of the resource</param>
+ /// <param name="End">The ending byte range</param>
+ /// <param name="RangeType">Specifies the type of content range to observe</param>
+ public readonly record struct HttpRange(ulong Start, ulong End, HttpRangeType RangeType)
+ {
+ /// <summary>
+ /// Gets a value indicating if the range is valid. A range is valid if
+ /// the start is less than or equal to the end.
+ /// </summary>
+ /// <param name="start">The starting range value</param>
+ /// <param name="end">The ending range value</param>
+ /// <returns>True if the range values are valid, false otherwise</returns>
+ public static bool IsValidRangeValue(ulong start, ulong end) => start <= end;
+ }
+} \ No newline at end of file
diff --git a/lib/Net.Http/src/Core/Response/DirectStream.cs b/lib/Net.Http/src/Helpers/HttpRangeType.cs
index 2406f0f..97d0721 100644
--- a/lib/Net.Http/src/Core/Response/DirectStream.cs
+++ b/lib/Net.Http/src/Helpers/HttpRangeType.cs
@@ -3,9 +3,9 @@
*
* Library: VNLib
* Package: VNLib.Net.Http
-* File: DirectStream.cs
+* File: HttpRangeType.cs
*
-* DirectStream.cs is part of VNLib.Net.Http which is part of the larger
+* 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
@@ -23,17 +23,30 @@
*/
using System;
-using System.Threading.Tasks;
-namespace VNLib.Net.Http.Core.Response
+namespace VNLib.Net.Http
{
-
- internal sealed class DirectStream : ReusableResponseStream, IDirectResponsWriter
+ /// <summary>
+ /// An enumeration of http range types to observe from http requests
+ /// </summary>
+ [Flags]
+ public enum HttpRangeType
{
- ///<inheritdoc/>
- public Task FlushAsync() => transport!.FlushAsync();
-
- ///<inheritdoc/>
- public ValueTask WriteAsync(ReadOnlyMemory<byte> buffer) => transport!.WriteAsync(buffer);
+ /// <summary>
+ /// DO NOT USE, NOT VALID
+ /// </summary>
+ None = 0,
+ /// <summary>
+ /// A range of bytes from the start of the resource
+ /// </summary>
+ FromStart = 1,
+ /// <summary>
+ /// A range of bytes from the end of the resource
+ /// </summary>
+ FromEnd = 2,
+ /// <summary>
+ /// A full range of bytes from the start to the end of the resource
+ /// </summary>
+ 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;
/// <summary>
- /// 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 <see cref="HttpConfig.CompressorManager"/> is set to null (compression is disabled).
/// </summary>
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
/// <summary>
/// The absolute request entity body size limit in bytes
/// </summary>
- public readonly int MaxUploadSize { get; init; } = 5 * 1000 * 1024;
+ public readonly long MaxUploadSize { get; init; } = 5 * 1000 * 1024;
/// <summary>
/// The maximum size in bytes allowed for an MIME form-data content type upload
/// </summary>
/// <remarks>Set to 0 to disabled mulit-part/form-data uploads</remarks>
- public readonly int MaxFormDataUploadSize { get; init; } = 40 * 1024;
+ public readonly int MaxFormDataUploadSize { get; init; } = 40 * 1024;
+
+ /// <summary>
+ /// The maximum number of file uploads allowed per request
+ /// </summary>
+ public readonly ushort MaxUploadsPerRequest { get; init; } = 5;
/// <summary>
/// The maximum response entity size in bytes for which the library will allow compresssing response data
/// </summary>
/// <remarks>Set this value to 0 to disable response compression</remarks>
- public readonly int CompressionLimit { get; init; } = 1000 * 1024;
+ public readonly long CompressionLimit { get; init; } = 1000 * 1024;
/// <summary>
/// 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.
/// </summary>
/// <remarks>Set to 0 to disable request processing. Causes perminant 503 results</remarks>
- public readonly int MaxOpenConnections { get; init; } = int.MaxValue;
+ public readonly int MaxOpenConnections { get; init; } = int.MaxValue;
/// <summary>
/// An <see cref="ILogProvider"/> for writing verbose request logs. Set to <c>null</c>
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; }
/// <summary>
- /// The parsed range header, or -1,-1 if the range header was not set
+ /// The parsed range header, check the <see cref="HttpRange.RangeType"/>
+ /// to determine if the range has been set
/// </summary>
- Tuple<long, long>? Range { get; }
+ HttpRange Range { get; }
/// <summary>
/// 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
/// <param name="code">Response status code</param>
/// <param name="type">MIME ContentType of data</param>
/// <param name="stream">Data to be sent to client</param>
+ /// <param name="length">Length of data to read from the stream and send to client</param>
/// <exception cref="IOException"></exception>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InvalidOperationException"></exception>
- void CloseResponse(HttpStatusCode code, ContentType type, Stream stream);
+ void CloseResponse(HttpStatusCode code, ContentType type, Stream stream, long length);
/// <summary>
/// Responds to a client with an in-memory <see cref="IMemoryResponseReader"/> containing data
@@ -86,6 +89,7 @@ namespace VNLib.Net.Http
/// <param name="code">The status code to set</param>
/// <param name="type">The entity content-type</param>
/// <param name="entity">The in-memory response data</param>
+ /// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InvalidOperationException"></exception>
void CloseResponse(HttpStatusCode code, ContentType type, IMemoryResponseReader entity);
@@ -99,9 +103,9 @@ namespace VNLib.Net.Http
void DangerousChangeProtocol(IAlternateProtocol protocolHandler);
/// <summary>
- /// Disables response compression
+ /// Sets an http server control mask to be applied to the current event flow
/// </summary>
- 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<byte> 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
/// <param name="code">The status code to return to the client</param>
/// <param name="ct">The response content type</param>
/// <param name="response">The stream response to return to the user</param>
+ /// <param name="length">The explicit length of the stream</param>
+ /// <returns>The <see cref="VfReturnType.VirtualSkip"/> operation result</returns>
+ public static VfReturnType VirtualClose(HttpEntity entity, HttpStatusCode code, ContentType ct, Stream response, long length)
+ {
+ entity.CloseResponse(code, ct, response, length);
+ return VfReturnType.VirtualSkip;
+ }
+
+ /// <summary>
+ /// Shortcut helper methods to a virtual skip response with a given status code,
+ /// and memory content to return to the client
+ /// </summary>
+ /// <param name="entity">The entity to close the connection for</param>
+ /// <param name="code">The status code to return to the client</param>
+ /// <param name="ct">The response content type</param>
+ /// <param name="response">The stream response to return to the user</param>
+ /// <param name="length">The explicit length of the stream</param>
/// <returns>The <see cref="VfReturnType.VirtualSkip"/> operation result</returns>
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");
}
+
+ /// <summary>
+ /// Sets the content-range header to the specified parameters
+ /// </summary>
+ /// <param name="entity"></param>
+ /// <param name="range">The http range used to return set the response header</param>
+ /// <param name="length">The total content length</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ 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<char> buffer = stackalloc char[64];
+ ForwardOnlyWriter<char> rangeBuilder = new(buffer);
+ //Build the range header in this format "bytes <begin>-<end>/<total>"
+ rangeBuilder.Append("bytes ");
+ rangeBuilder.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();
+ }
+
/// <summary>
/// Is the connection requesting cors
/// </summary>
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
/// <param name="data">The data to straem to the client as an attatcment</param>
/// <param name="ct">The <see cref="ContentType"/> that represents the file</param>
/// <param name="fileName">The name of the file to attach</param>
+ /// <param name="length">Explicit length of the stream data</param>
/// <exception cref="ContentTypeUnacceptableException"></exception>
[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);
}
-
+
+
/// <summary>
/// Close a response to a connection with a character buffer using the server wide
/// <see cref="ConnectionInfo.Encoding"/> 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<char> 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);
+ /// <summary>
+ /// Sets the <see cref="HttpControlMask.CompressionDisabed"/> flag on the current
+ /// <see cref="IHttpEvent"/> instance to disable dynamic compression on the response.
+ /// </summary>
+ /// <param name="entity"></param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void DisableCompression(this IHttpEvent entity) => entity.SetControlFlag(HttpControlMask.CompressionDisabed);
/// <summary>
/// Attempts to upgrade the connection to a websocket, if the setup fails, it sets up the response to the client accordingly.
diff --git a/lib/Plugins.Essentials/src/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
///<inheritdoc/>
///<exception cref="ContentTypeUnacceptableException"></exception>
[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);
}
///<inheritdoc/>
@@ -209,7 +210,7 @@ namespace VNLib.Plugins.Essentials
///<inheritdoc/>
[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