Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
CypherPotato committed Dec 22, 2024
1 parent 002f2b8 commit 970a91d
Show file tree
Hide file tree
Showing 12 changed files with 375 additions and 100 deletions.
24 changes: 21 additions & 3 deletions extensions/Sisk.IniConfiguration/IniDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@
namespace Sisk.IniConfiguration;

/// <summary>
/// Represents an INI configuration document.
/// Represents an INI document.
/// </summary>
public sealed class IniDocument {

/// <summary>
/// Represents an empty <see cref="IniDocument"/> with no entry on it.
/// </summary>
public static readonly IniDocument Empty = new IniDocument ( Array.Empty<IniSection> () );

/// <summary>
/// Gets all INI sections defined in this INI document.
/// </summary>
Expand All @@ -24,7 +30,15 @@ public sealed class IniDocument {
/// <summary>
/// Gets the global INI section, which is the primary section in the document.
/// </summary>
public IniSection Global { get => this.Sections [ 0 ]; }
public IniSection Global {
get {
if (this.Sections.Count == 0) {
return new IniSection ( IniReader.INITIAL_SECTION_NAME, Array.Empty<(string, string)> () );
}
else
return this.Sections [ 0 ];
}
}

/// <summary>
/// Creates an new <see cref="IniDocument"/> document from the specified
Expand All @@ -43,7 +57,11 @@ public static IniDocument FromString ( string iniConfiguration ) {
/// </summary>
/// <param name="filePath">The absolute or relative file path to the INI document.</param>
/// <param name="encoding">Optional. The encoding used to read the file. Defaults to UTF-8.</param>
public static IniDocument FromFile ( string filePath, Encoding? encoding = null ) {
/// <param name="throwIfNotExists">Optional. Defines whether this method should throw if the specified file doens't exists or return an empty INI document.</param>
public static IniDocument FromFile ( string filePath, Encoding? encoding = null, bool throwIfNotExists = true ) {
if (!throwIfNotExists && !File.Exists ( filePath ))
return IniDocument.Empty;

using TextReader reader = new StreamReader ( filePath, encoding ?? Encoding.UTF8 );
using IniReader parser = new IniReader ( reader );
return parser.Read ();
Expand Down
4 changes: 3 additions & 1 deletion extensions/Sisk.IniConfiguration/Serializer/IniReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ namespace Sisk.IniConfiguration.Serializer;
public sealed class IniReader : IDisposable {
internal static readonly StringComparer IniNamingComparer = StringComparer.InvariantCultureIgnoreCase;

internal const string INITIAL_SECTION_NAME = "__SISKINIGLOBAL__";

private readonly TextReader reader;
private bool disposedValue;

Expand All @@ -40,7 +42,7 @@ public IniReader ( TextReader reader ) {
public IniDocument Read () {
this.ThrowIfDisposed ();

string lastSectionName = "__SISKINIGLOBAL__";
string lastSectionName = INITIAL_SECTION_NAME;
List<(string, string)> items = new List<(string, string)> ();
List<IniSection> creatingSections = new List<IniSection> ();

Expand Down
6 changes: 3 additions & 3 deletions extensions/Sisk.IniConfiguration/Sisk.IniConfiguration.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
<PackageTags>http-server,http,web framework</PackageTags>
<RepositoryType>git</RepositoryType>

<AssemblyVersion>1.3.0</AssemblyVersion>
<FileVersion>1.3.0</FileVersion>
<Version>1.3.0</Version>
<AssemblyVersion>1.3.0.1</AssemblyVersion>
<FileVersion>1.3.0.1</FileVersion>
<Version>1.3.0.1</Version>

<NeutralLanguage>en</NeutralLanguage>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
Expand Down
50 changes: 50 additions & 0 deletions tcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Sisk managed HTTP listener

This folder contains the code for the implementation of the Sisk HTTP/1.1 listener. It is a managed alternative to the [HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0) of .NET. This implementation exists because:

- We do not know how long Microsoft will want to maintain HttpListener as a component of .NET.
- It is not possible to use HttpListener with SSL natively, only with a reverse proxy or with IIS on Windows.
- The implementation of HttpListener outside of Windows is awful, but stable. This implementation is an attempt to create something more performant, closer or better than Kestrel speeds.

## How to use

For now, this implementation does not even have a package or is used in Sisk.HttpServer, but it can be used with:

```csharp
static void Main ( string [] args ) {
HttpHost host = new HttpHost ( 5555, HandleSession );

// optional properties to run SSL
host.HttpsOptions = new HttpsOptions ( CertificateUtil.CreateTrustedDevelopmentCertificate () );

Console.WriteLine ( "server running at : http://localhost:5555/" );

host.Start ();
Thread.Sleep ( -1 );
}

public static void HandleSession ( HttpSession session ) {
// handle the request here
}
```

## Implementation status

This package is expected to supersede Sisk.SslProxy and deprecate it.

The current status of the implementation is:

| Resource | Status | Notes |
| ------- | ------ | ----------- |
| Base HTTP/1.1 Reader | Functional | |
| HTTPS | Functional | |
| Expect-100 header | Not implemented | There is already an implementation in Sisk.SslProxy. |
| Chunked transfer-encoding | Not implemented | |
| SSE/Response content streaming | Not implemented | |
| Web Sockets | Not implemented | |

Everything in this project is still super experimental and should never be used in production.

## License

The same as the Sisk project (MIT).
62 changes: 39 additions & 23 deletions tcp/Sisk.ManagedHttpListener/HttpConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,63 @@
// Repository: https://github.com/sisk-http/core

using System.Buffers;
using System.Net.Sockets;
using Sisk.ManagedHttpListener.HttpSerializer;

namespace Sisk.ManagedHttpListener;

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

#if DEBUG
public readonly int Id = Random.Shared.Next ( 0, ushort.MaxValue );
#else
public readonly int Id = 0;
#endif

public const int REQUEST_BUFFER_SIZE = 4096;
public const int RESPONSE_BUFFER_SIZE = 8192;

public HttpAction Action { get; set; }

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

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

bool connectionCloseRequested = false;
byte [] buffer = ArrayPool<byte>.Shared.Rent ( REQUEST_BUFFER_SIZE );

while (this._connectionStream.CanRead && !this.disposedValue) {
HttpRequestReader requestReader = new HttpRequestReader ( this._connectionStream );
HttpRequestBase? nextRequest = requestReader.ReadHttpRequest ();

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

try {

var readRequestState = await requestReader.ReadHttpRequest ();
var nextRequest = readRequestState.Item2;

if (nextRequest is null) {
Logger.LogInformation ( $"couldn't read request" );
return 1;
return readRequestState.Item1 switch {
HttpRequestReadState.StreamZero => HttpConnectionState.ConnectionClosedByStreamRead,
_ => HttpConnectionState.BadRequest
};
}

Logger.LogInformation ( $"[{this.Id}] Received \"{nextRequest.Method} {nextRequest.Path}\"" );
HttpSession managedSession = new HttpSession ( nextRequest, this._connectionStream );

this.Action ( managedSession );

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

responseStream = managedSession.Response.ResponseStream;
if (responseStream is not null) {
Expand All @@ -57,34 +79,28 @@ public int HandleConnectionEvents () {
managedSession.Response.Headers.Set ( ("Content-Length", "0") );
}

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

if (responseStream is not null) {
responseStream.CopyTo ( this._connectionStream );
responseStream.Dispose ();
return HttpConnectionState.ResponseWriteException;
}

if (responseStream is not null)
await responseStream.CopyToAsync ( this._connectionStream );

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

if (!managedSession.KeepAlive) {
if (connectionCloseRequested) {
break;
}
}
finally {
responseStream?.Dispose ();
if (nextRequest is not null)
ArrayPool<byte>.Shared.Return ( nextRequest.BufferedContent );
ArrayPool<byte>.Shared.Return ( buffer );
}
}

return 0;
return HttpConnectionState.ConnectionClosed;
}

private void Dispose ( bool disposing ) {
Expand Down
33 changes: 33 additions & 0 deletions tcp/Sisk.ManagedHttpListener/HttpConnectionState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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: HttpConnectionState.cs
// Repository: https://github.com/sisk-http/core

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sisk.ManagedHttpListener;

internal enum HttpConnectionState {
ConnectionClosed = 0,
ConnectionClosedByStreamRead = 1,

UnhandledException = 10,

BadRequest = 20,

ResponseWriteException = 30,
}

internal enum HttpRequestReadState {
RequestRead = 0,
StreamZero = 1,
StreamError = 2
}
Loading

0 comments on commit 970a91d

Please sign in to comment.