Skip to content

Commit

Permalink
feat: API enhancements, add custom experimental httplistener
Browse files Browse the repository at this point in the history
  • Loading branch information
CypherPotato committed Nov 25, 2024
1 parent 1148fd8 commit 93f8ad8
Show file tree
Hide file tree
Showing 19 changed files with 692 additions and 10 deletions.
32 changes: 31 additions & 1 deletion src/Entity/HttpHeaderCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// Repository: https://github.com/sisk-http/core

using Sisk.Core.Http;
using System.Net;
using Header = Sisk.Core.Http.HttpKnownHeaderNames;

namespace Sisk.Core.Entity;
Expand All @@ -17,7 +18,7 @@ namespace Sisk.Core.Entity;
/// </summary>
public sealed class HttpHeaderCollection : StringKeyStore
{
static readonly StringComparer _comparer = StringComparer.InvariantCultureIgnoreCase;
static readonly StringComparer _comparer = StringComparer.OrdinalIgnoreCase;

/// <summary>
/// Create an new instance of the <see cref="HttpHeaderCollection"/> class.
Expand All @@ -26,6 +27,35 @@ public HttpHeaderCollection() : base(_comparer)
{
}

/// <summary>
/// Create an new instance of the <see cref="HttpHeaderCollection"/> class with values from another
/// collection.
/// </summary>
/// <param name="items">The inner collection to add to this collection.</param>
public HttpHeaderCollection(IDictionary<string, string[]> items) : base(_comparer)
{
this.AddRange(items);
}

/// <summary>
/// Create an new instance of the <see cref="HttpHeaderCollection"/> class with values from another
/// collection.
/// </summary>
/// <param name="items">The inner collection to add to this collection.</param>
public HttpHeaderCollection(IDictionary<string, string?> items) : base(_comparer)
{
this.AddRange(items);
}

/// <summary>
/// Create an new instance of the <see cref="HttpHeaderCollection"/> class with values from another
/// collection.
/// </summary>
/// <param name="items">The inner collection to add to this collection.</param>
public HttpHeaderCollection(WebHeaderCollection items) : base(_comparer)
{
this.AddRange(FromNameValueCollection(items));
}
#region Helper properties

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Entity/MultipartFormCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal MultipartFormCollection(IEnumerable<MultipartObject> items)
}

/// <summary>
/// Gets the last form item by their name. The search is case-insensitive.
/// Gets the last form item by their name. This search is case-insensitive.
/// </summary>
/// <param name="name">The form item name.</param>
public MultipartObject? GetItem(string name)
Expand All @@ -35,7 +35,7 @@ internal MultipartFormCollection(IEnumerable<MultipartObject> items)
}

/// <summary>
/// Gets all form items that shares the specified name. The search is case-insensitive.
/// Gets all form items that shares the specified name. This search is case-insensitive.
/// </summary>
/// <param name="name">The form item name.</param>
/// <returns>An array of <see cref="MultipartObject"/> with the specified name.</returns>
Expand Down
7 changes: 4 additions & 3 deletions src/Entity/MultipartFormReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ string ReadLine()

Span<byte> ReadContent()
{
var boundarySpan = this.boundaryBytes.AsSpan();
int boundaryLen = this.boundaryBytes.Length;
int istart = this.position;

Expand All @@ -151,14 +152,14 @@ Span<byte> ReadContent()

if ((this.position - istart) > boundaryLen)
{
if (this.bytes[(this.position - boundaryLen)..this.position].SequenceEqual(this.boundaryBytes))
if (this.bytes[(this.position - boundaryLen)..this.position].AsSpan().SequenceCompareTo(boundarySpan) == 0)
{
break;
}
}
}

this.position -= boundaryLen + this.nlbytes.Length + 2 /* the boundary "--" construct */;
this.position -= boundaryLen + this.nlbytes.Length + 2 /* +2 represents the boundary "--" construct */;

return this.bytes.AsSpan()[istart..this.position];
}
Expand All @@ -182,7 +183,7 @@ NameValueCollection ReadHeaders()
return headers;
}

unsafe void ReadNextBoundary()
void ReadNextBoundary()
{
Span<byte> boundaryBlock = stackalloc byte[this.boundaryBytes.Length + 2];
int nextLine = this.Read(boundaryBlock);
Expand Down
3 changes: 1 addition & 2 deletions src/Entity/MultipartObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,7 @@ internal static MultipartFormCollection ParseMultipartObjects(HttpRequest req)
throw new InvalidOperationException(SR.MultipartObject_BoundaryMissing);
}

byte[] boundaryBytes = Encoding.UTF8.GetBytes(boundary);

byte[] boundaryBytes = req.RequestEncoding.GetBytes(boundary);
if (req.baseServer.ServerConfiguration.Flags.EnableNewMultipartFormReader == true)
{
MultipartFormReader reader = new MultipartFormReader(req.RawBody, boundaryBytes, req.RequestEncoding, req.baseServer.ServerConfiguration.ThrowExceptions);
Expand Down
23 changes: 23 additions & 0 deletions src/Entity/StringKeyStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ public StringKeyStore(IEqualityComparer<string> comparer)
this.items = new();
}

/// <summary>
/// Initializes a new instance of the <see cref="StringKeyStore"/> class,
/// </summary>
/// <param name="comparer">The comparer used for key equality.</param>
/// <param name="items">The inner collection to add to this instance.</param>
public StringKeyStore(IEqualityComparer<string> comparer, IDictionary<string, string[]>? items)
{
this.Comparer = StringComparer.CurrentCulture;
this.items = new();
if (items != null)
this.AddRange(items);
}

#region Internal methods

internal void AddInternal(string key, IEnumerable<string> values)
Expand Down Expand Up @@ -272,6 +285,16 @@ public void AddRange(IEnumerable<KeyValuePair<string, string[]>> items)
this.Add(item);
}

/// <summary>
/// Adds the elements of the specified collection to the end of this collection.
/// </summary>
/// <param name="items">The collection whose items should be added to the end of this collection.</param>
public void AddRange(IEnumerable<KeyValuePair<string, string?>> items)
{
foreach (KeyValuePair<string, string?> item in items)
this.Add(item.Key, item.Value ?? string.Empty);
}

/// <summary>
/// Sets the elements of the specified collection, replacing existing values.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/StringValueCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public sealed class StringValueCollection : StringKeyStore
/// </summary>
public StringValueCollection(IDictionary<string, string?> values) : base(StringComparer.InvariantCultureIgnoreCase)
{
base.AddRange(values.Select(k => new KeyValuePair<string, string[]>(k.Key, [k.Value ?? string.Empty])));
base.AddRange(values);
this.paramName = "StringValue";
}

Expand Down
2 changes: 1 addition & 1 deletion src/Http/HttpServer__Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ private void ProcessRequest(HttpListenerContext context)
return;
}

string dnsSafeHost = baseRequest.UserHostName;
string dnsSafeHost = baseRequest.Url.Host;
if (this.ServerConfiguration.ForwardingResolver is ForwardingResolver fr)
{
dnsSafeHost = fr.OnResolveRequestHost(request, dnsSafeHost);
Expand Down
3 changes: 3 additions & 0 deletions tcp/Sisk.ManagedHttpListener/HttpAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Sisk.ManagedHttpListener;

public delegate void HttpAction(HttpSession session);
120 changes: 120 additions & 0 deletions tcp/Sisk.ManagedHttpListener/HttpConnection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using Sisk.ManagedHttpListener.HttpSerializer;

namespace Sisk.ManagedHttpListener;

public sealed class HttpConnection : IDisposable
{
private readonly Stream _connectionStream;
private bool disposedValue;

public HttpAction Action { get; set; }

public HttpConnection(Stream connectionStream, HttpAction action)
{
_connectionStream = connectionStream;
Action = action;
}

public int HandleConnectionEvents()
{
ObjectDisposedException.ThrowIf(disposedValue, this);

Span<byte> memRequestLine = stackalloc byte[8192];

while (_connectionStream.CanRead && !disposedValue)
{
//try
//{
using var bufferedStreamSession = new Streams.HttpBufferedStream(_connectionStream);

if (!HttpRequestSerializer.TryReadHttp1Request(
bufferedStreamSession,
memRequestLine,
out var method,
out var path,
out var reqContentLength,
out var messageSize,
out var headers,
out var expectContinue))
{
Logger.LogInformation($"couldn't read request");
return 1;
}

HttpSession.HttpRequest managedRequest = new HttpSession.HttpRequest(method, path, reqContentLength, headers, _connectionStream);
HttpSession managedSession = new HttpSession(managedRequest, _connectionStream);

Action(managedSession);

if (!managedSession.KeepAlive)
managedSession.Response.Headers.Set(("Connection", "Close"));

Stream? responseStream = managedSession.Response.ResponseStream;
if (responseStream is not null)
{
if (responseStream.CanSeek)
{
managedSession.Response.Headers.Set(("Content-Length", responseStream.Length.ToString()));
}
else
{
// implement chunked-encodind
}
}
else
{
managedSession.Response.Headers.Set(("Content-Length", "0"));
}

if (!HttpResponseSerializer.TryWriteHttp1Response(
_connectionStream,
managedSession.Response.StatusCode,
managedSession.Response.StatusDescription,
managedSession.Response.Headers))
{
Logger.LogInformation($"couldn't write response");
return 2;
}

if (responseStream is not null)
{
responseStream.CopyTo(_connectionStream);
responseStream.Dispose();
}

_connectionStream.Flush();

if (!managedSession.KeepAlive)
{
break;
}
//}
//catch (Exception ex)
//{
// Logger.LogInformation($"unhandled exception: {ex.Message}");
// return 3;
//}
}

return 0;
}

private void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_connectionStream.Dispose();
}

disposedValue = true;
}
}

public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
26 changes: 26 additions & 0 deletions tcp/Sisk.ManagedHttpListener/HttpHeaderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// The Sisk Framework source code
// Copyright (c) 2023 PROJECT PRINCIPIUM
//
// The code below is licensed under the MIT license as
// of the date of its publication, available at
//
// File name: HttpHeaderExtensions.cs
// Repository: https://github.com/sisk-http/core

namespace Sisk.ManagedHttpListener;

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

headers.Add(header);
}
}
62 changes: 62 additions & 0 deletions tcp/Sisk.ManagedHttpListener/HttpHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Net;
using System.Net.Sockets;

namespace Sisk.ManagedHttpListener;

public sealed class HttpHost : IDisposable
{
private readonly TcpListener _listener;
private bool disposedValue;

public HttpAction ActionHandler { get; }
public bool IsDisposed { get => disposedValue; }
public int Port { get; set; } = 8080;

public HttpHost(int port, HttpAction actionHandler)
{
_listener = new TcpListener(new IPEndPoint(IPAddress.Any, port));
ActionHandler = actionHandler;
}

public void Start()
{
ObjectDisposedException.ThrowIf(disposedValue, this);

_listener.Start();
_listener.BeginAcceptTcpClient(ReceiveClient, null);
}

private void ReceiveClient(IAsyncResult result)
{
_listener.BeginAcceptTcpClient(ReceiveClient, null);
using (TcpClient client = _listener.EndAcceptTcpClient(result))
{
Stream clientStream = client.GetStream();

using (HttpConnection connection = new HttpConnection(clientStream, ActionHandler))
{
connection.HandleConnectionEvents();
}
}
}

private void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_listener.Dispose();
}

disposedValue = true;
}
}

public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
Loading

0 comments on commit 93f8ad8

Please sign in to comment.