Skip to content

Commit

Permalink
http/1 wip
Browse files Browse the repository at this point in the history
  • Loading branch information
CypherPotato committed Dec 25, 2024
1 parent 132ffa4 commit e7940f3
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 68 deletions.
7 changes: 4 additions & 3 deletions tcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ The current status of the implementation is:

| Resource | Status | Notes |
| ------- | ------ | ----------- |
| Base HTTP/1.1 Reader | Needs testing | |
| HTTPS | Needs testing | |
| Chunked transfer-encoding | Needs testing | |
| Base HTTP/1.1 Reader | OK - Needs testing | |
| HTTPS | OK - Needs testing | |
| Chunked transfer-encoding | OK - Needs testing | Only for responses. |
| Gzip transfer encoding | Not implemented | Implement for both request and response. |
| Expect-100 header | Not implemented | There is already an implementation in Sisk.SslProxy. |
| SSE/Response content streaming | Not implemented | |
| Web Sockets | Not implemented | |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// The Sisk Framework source code
// Copyright (c) 2024- PROJECT PRINCIPIUM and all Sisk contributors
//
// The code below is licensed under the MIT license as
// of the date of its publication, available at
//
// File name: ArrayPoolExtensions.cs
// Repository: https://github.com/sisk-http/core

using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sisk.ManagedHttpListener.HighPerformance;
internal static class ArrayPoolExtensions {

// forked from https://github.com/CommunityToolkit/dotnet/blob/main/src/CommunityToolkit.HighPerformance/Extensions/ArrayPoolExtensions.cs

/// <summary>
/// Changes the number of elements of a rented one-dimensional array to the specified new size.
/// </summary>
/// <typeparam name="T">The type of items into the target array to resize.</typeparam>
/// <param name="pool">The target <see cref="ArrayPool{T}"/> instance to use to resize the array.</param>
/// <param name="array">The rented <typeparamref name="T"/> array to resize, or <see langword="null"/> to create a new array.</param>
/// <param name="newSize">The size of the new array.</param>
/// <param name="clearArray">Indicates whether the contents of the array should be cleared before reuse.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="newSize"/> is less than 0.</exception>
/// <remarks>When this method returns, the caller must not use any references to the old array anymore.</remarks>
public static void Resize<T> ( this ArrayPool<T> pool, [NotNull] ref T []? array, int newSize, bool clearArray = false ) {
// If the old array is null, just create a new one with the requested size
if (array is null) {
array = pool.Rent ( newSize );

return;
}

// If the new size is the same as the current size, do nothing
if (array.Length == newSize) {
return;
}

// Rent a new array with the specified size, and copy as many items from the current array
// as possible to the new array. This mirrors the behavior of the Array.Resize API from
// the BCL: if the new size is greater than the length of the current array, copy all the
// items from the original array into the new one. Otherwise, copy as many items as possible,
// until the new array is completely filled, and ignore the remaining items in the first array.
T [] newArray = pool.Rent ( newSize );
int itemsToCopy = Math.Min ( array.Length, newSize );

Array.Copy ( array, 0, newArray, 0, itemsToCopy );

pool.Return ( array, clearArray );

array = newArray;
}
}
36 changes: 24 additions & 12 deletions tcp/Sisk.ManagedHttpListener/HttpConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
// Repository: https://github.com/sisk-http/core

using System.Buffers;
using System.IO.Compression;
using System.Net.Sockets;
using Sisk.ManagedHttpListener.HttpSerializer;
using Sisk.ManagedHttpListener.Streams;

Expand All @@ -35,7 +33,7 @@ public HttpConnection ( Stream connectionStream, HttpAction action ) {
this.Action = action;
}

public async ValueTask<HttpConnectionState> HandleConnectionEvents () {
public async Task<HttpConnectionState> HandleConnectionEvents () {
ObjectDisposedException.ThrowIf ( this.disposedValue, this );

bool connectionCloseRequested = false;
Expand All @@ -45,6 +43,7 @@ public async ValueTask<HttpConnectionState> HandleConnectionEvents () {

HttpRequestReader requestReader = new HttpRequestReader ( this._connectionStream, ref buffer );
Stream? responseStream = null;
byte []? responseBytes = null;

try {

Expand All @@ -65,32 +64,45 @@ public async ValueTask<HttpConnectionState> HandleConnectionEvents () {

if (!managedSession.KeepAlive || !nextRequest.CanKeepAlive) {
connectionCloseRequested = true;
managedSession.Response.Headers.Set ( ("Connection", "close") );
managedSession.Response.Headers.Set ( new HttpHeader ( "Connection", "close" ) );
}

responseStream = managedSession.Response.ResponseStream;
if (responseStream is not null) {
if (responseStream.CanSeek) {
managedSession.Response.Headers.Set ( ("Content-Length", responseStream.Length.ToString ()) );
if (managedSession.Response.ResponseStream is Stream { } s) {
responseStream = s;

if (managedSession.Response.TransferEncoding.HasFlag ( TransferEncoding.Chunked ) || !responseStream.CanSeek) {
managedSession.Response.Headers.Set ( new HttpHeader ( "Transfer-Encoding", "chunked" ) );
responseStream = new HttpChunkedStream ( responseStream );
}

else {
managedSession.Response.Headers.Set ( ("Transfer-Encoding", "chunked") );
responseStream = new HttpChunkedStream ( responseStream );
managedSession.Response.Headers.Set ( new HttpHeader ( "Content-Length", responseStream.Length.ToString () ) );
}
}

else if (managedSession.Response.ResponseBytes is byte [] b) {
responseBytes = b;
managedSession.Response.Headers.Set ( new HttpHeader ( "Content-Length", b.Length.ToString () ) );
}

else {
managedSession.Response.Headers.Set ( ("Content-Length", "0") );
managedSession.Response.Headers.Set ( new HttpHeader ( "Content-Length", "0" ) );
}

if (await HttpResponseSerializer.WriteHttpResponseHeaders ( this._connectionStream, managedSession.Response ) == false) {

return HttpConnectionState.ResponseWriteException;
}

if (responseStream is not null)
if (responseStream is not null) {
await responseStream.CopyToAsync ( this._connectionStream );
}
else if (responseBytes is not null) {
await this._connectionStream.WriteAsync ( responseBytes );
}

this._connectionStream.Flush ();

Logger.LogInformation ( $"[{this.Id}] Response sent: {managedSession.Response.StatusCode} {managedSession.Response.StatusDescription}" );

if (connectionCloseRequested) {
Expand Down
11 changes: 8 additions & 3 deletions tcp/Sisk.ManagedHttpListener/HttpHeaderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@
// Repository: https://github.com/sisk-http/core

using System.Collections;
using System.Runtime.InteropServices;
using System.Text;
using Sisk.ManagedHttpListener.HttpSerializer;

namespace Sisk.ManagedHttpListener;

internal static class HttpHeaderExtensions {
public static void Set ( this List<(string, string)> headers, (string, string) header ) {
public static void Set ( this List<HttpHeader> headers, in HttpHeader header ) {
lock (((ICollection) headers).SyncRoot) {
for (int i = headers.Count - 1; i >= 0; i--) {
if (StringComparer.OrdinalIgnoreCase.Compare ( headers [ i ].Item1, header.Item1 ) == 0) {

var span = CollectionsMarshal.AsSpan ( headers );
for (int i = span.Length - 1; i >= 0; i--) {
if (Ascii.EqualsIgnoreCase ( span [ i ].NameBytes.Span, header.NameBytes.Span )) {
headers.RemoveAt ( i );
}
}
Expand Down
3 changes: 1 addition & 2 deletions tcp/Sisk.ManagedHttpListener/HttpHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ private async void ReceiveClient ( IAsyncResult result ) {
await this.writerQueue.WriteAsync ( client );
}

private async Task HandleTcpClient ( object? obj ) {
TcpClient client = (TcpClient) obj!;
private async Task HandleTcpClient ( TcpClient client ) {
try {
{ // setup the tcpclient
client.NoDelay = true;
Expand Down
4 changes: 2 additions & 2 deletions tcp/Sisk.ManagedHttpListener/HttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ public sealed class HttpRequest {
public string Method { get; }
public string Path { get; }
public long ContentLength { get; }
public IReadOnlyList<(string, string)> Headers { get; }
public HttpHeader [] Headers { get; }
public Stream ContentStream { get; }

internal HttpRequest ( HttpRequestBase request, HttpRequestStream requestStream ) {
this.ContentLength = requestStream.Length;

this.Method = request.Method;
this.Path = request.Path;
this.Headers = request.Headers.AsReadOnly ();
this.Headers = request.Headers;
this.ContentStream = requestStream;
}
}
15 changes: 11 additions & 4 deletions tcp/Sisk.ManagedHttpListener/HttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,28 @@
// File name: HttpResponse.cs
// Repository: https://github.com/sisk-http/core

using Sisk.ManagedHttpListener.HttpSerializer;

namespace Sisk.ManagedHttpListener;

public sealed class HttpResponse {
public int StatusCode { get; set; }
public string StatusDescription { get; set; }
public List<(string, string)> Headers { get; set; }
public List<HttpHeader> Headers { get; set; }

public TransferEncoding TransferEncoding { get; set; }

// MUST SPECIFY ResponseStream OR ResponseBytes, NOT BOTH
public Stream? ResponseStream { get; set; }
public byte []? ResponseBytes { get; set; }

internal HttpResponse () {
this.StatusCode = 200;
this.StatusDescription = "Ok";
this.Headers = new List<(string, string)>
this.Headers = new List<HttpHeader>
{
("Date", DateTime.Now.ToString("R")),
("Server", "Sisk")
new HttpHeader ("Date", DateTime.Now.ToString("R")),
new HttpHeader ("Server", "Sisk")
};
}
}
39 changes: 39 additions & 0 deletions tcp/Sisk.ManagedHttpListener/HttpSerializer/HttpHeader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// The Sisk Framework source code
// Copyright (c) 2024- PROJECT PRINCIPIUM and all Sisk contributors
//
// The code below is licensed under the MIT license as
// of the date of its publication, available at
//
// File name: HttpHeader.cs
// Repository: https://github.com/sisk-http/core

using System.Text;

namespace Sisk.ManagedHttpListener.HttpSerializer;

public readonly struct HttpHeader {
internal readonly ReadOnlyMemory<byte> NameBytes;
internal readonly ReadOnlyMemory<byte> ValueBytes;

public HttpHeader ( ReadOnlyMemory<byte> nameBytes, ReadOnlyMemory<byte> valueBytes ) {
this.NameBytes = nameBytes;
this.ValueBytes = valueBytes;
}

public HttpHeader ( string name, string value ) {
this.NameBytes = Encoding.UTF8.GetBytes ( name );
this.ValueBytes = Encoding.UTF8.GetBytes ( value );
}

public string Name {
get {
return Encoding.UTF8.GetString ( this.NameBytes.Span );
}
}

public string Value {
get {
return Encoding.UTF8.GetString ( this.ValueBytes.Span );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class HttpRequestBase {
public required string Path;
public required string Version;

public required List<(string, string)> Headers;
public required HttpHeader [] Headers;

public long ContentLength;

Expand Down
31 changes: 13 additions & 18 deletions tcp/Sisk.ManagedHttpListener/HttpSerializer/HttpRequestReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using CommunityToolkit.HighPerformance;
using Sisk.ManagedHttpListener.HighPerformance;

namespace Sisk.ManagedHttpListener.HttpSerializer;

Expand All @@ -26,14 +26,12 @@ sealed class HttpRequestReader {
const byte CARRIAGE_RETURN = 0x0D; //\r
const byte DOUBLE_DOTS = 0x3A; // :

static Encoding HeaderEncoding = Encoding.UTF8;

public HttpRequestReader ( Stream stream, ref byte [] buffer ) {
this._stream = stream;
this.buffer = buffer;
}

public async ValueTask<(HttpRequestReadState, HttpRequestBase?)> ReadHttpRequest () {
public async Task<(HttpRequestReadState, HttpRequestBase?)> ReadHttpRequest () {
try {

int read = await this._stream.ReadAsync ( this.buffer );
Expand Down Expand Up @@ -87,7 +85,7 @@ public HttpRequestReader ( Stream stream, ref byte [] buffer ) {
bool keepAliveEnabled = true;
long contentLength = 0;

List<(string, string)> headers = new List<(string, string)> ( 16 );
List<HttpHeader> headers = new List<HttpHeader> ( 64 );

ref byte firstByte = ref MemoryMarshal.GetReference ( buffer );
for (int i = 0; i < length; i++) {
Expand Down Expand Up @@ -124,7 +122,7 @@ public HttpRequestReader ( Stream stream, ref byte [] buffer ) {
// checks whether the current buffer has all the request headers. if not, read more data from the buffer
int bufferLength = buffer.Length;
if (i + BUFFER_LOOKAHEAD_OFFSET > bufferLength && !requestStreamFinished) {
ArrayPoolExtensions.Resize ( ArrayPool<byte>.Shared, ref inputBuffer, bufferLength * 2, clearArray: false );
ArrayPool<byte>.Shared.Resize ( ref inputBuffer, inputBuffer.Length * 2, clearArray: false );
int count = inputBuffer.Length - bufferLength;
int read = this._stream.Read ( inputBuffer, bufferLength - 1, count );
if (read > 0) {
Expand All @@ -151,17 +149,14 @@ public HttpRequestReader ( Stream stream, ref byte [] buffer ) {
headerLineName = headerLine [ 0..headerLineSepIndex ];
headerLineValue = headerLine [ (headerLineSepIndex + 2).. ]; // +2 = : and the space

string headerName = HeaderEncoding.GetString ( headerLineName );
string headerValue = HeaderEncoding.GetString ( headerLineValue );

if (string.Compare ( headerName, "Content-Length", StringComparison.OrdinalIgnoreCase ) == 0) {
contentLength = long.Parse ( headerValue );
if (Ascii.EqualsIgnoreCase ( headerLineName, "Content-Length"u8 )) {
contentLength = long.Parse ( Encoding.ASCII.GetString ( headerLineValue ) );
}
else if (string.Compare ( headerName, "Connection", StringComparison.OrdinalIgnoreCase ) == 0) {
keepAliveEnabled = string.Compare ( headerValue, "close", StringComparison.Ordinal ) != 0;
else if (Ascii.EqualsIgnoreCase ( headerLineName, "Connection"u8 )) {
keepAliveEnabled = !headerLineValue.SequenceEqual ( "close"u8 );
}

headers.Add ( (headerName, headerValue) );
headers.Add ( new HttpHeader ( headerLineName.ToArray (), headerLineValue.ToArray () ) );
}

break;
Expand All @@ -175,10 +170,10 @@ public HttpRequestReader ( Stream stream, ref byte [] buffer ) {
BufferedContent = inputBuffer,
BufferHeaderIndex = headerSize,

Headers = headers,
Method = HeaderEncoding.GetString ( method ),
Path = HeaderEncoding.GetString ( path ),
Version = HeaderEncoding.GetString ( version ),
Headers = headers.ToArray (),
Method = Encoding.ASCII.GetString ( method ),
Path = Encoding.ASCII.GetString ( path ),
Version = Encoding.ASCII.GetString ( version ),

ContentLength = contentLength,
CanKeepAlive = keepAliveEnabled
Expand Down
Loading

0 comments on commit e7940f3

Please sign in to comment.