Skip to content

Commit 1e414e3

Browse files
committed
wip: cadente, 77/* coverage
1 parent 11d0358 commit 1e414e3

18 files changed

+596
-416
lines changed

cadente/Sisk.Cadente.CoreEngine/CadenteHttpServerEngine.cs

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,8 @@
88
// File name: CadenteHttpServerEngine.cs
99
// Repository: https://github.com/sisk-http/core
1010

11-
using System;
12-
using System.Collections.Concurrent;
13-
using System.Collections.Generic;
14-
using System.Linq;
1511
using System.Net;
16-
using System.Net.Sockets;
17-
using System.Text;
18-
using System.Threading.Tasks;
12+
using System.Threading.Channels;
1913
using Sisk.Core.Http.Engine;
2014

2115
namespace Sisk.Cadente.CoreEngine {
@@ -32,8 +26,11 @@ public sealed class CadenteHttpServerEngine : HttpServerEngine, IDisposable {
3226
private bool isDisposed;
3327

3428
private Action<HttpHost>? setupHostAction;
35-
private readonly ConcurrentQueue<TaskCompletionSource<HttpServerEngineContext>> _pendingContextRequests = new ();
36-
private readonly ConcurrentQueue<HttpServerEngineContext> _readyContexts = new ();
29+
private Channel<CadenteHttpServerEngineContext> _pendingContexts = Channel.CreateBounded<CadenteHttpServerEngineContext> ( new BoundedChannelOptions ( capacity: Environment.ProcessorCount * 512 ) {
30+
AllowSynchronousContinuations = false,
31+
SingleReader = true,
32+
SingleWriter = false
33+
} );
3734

3835
/// <summary>
3936
/// Initializes a new instance of the <see cref="CadenteHttpServerEngine"/> class.
@@ -56,6 +53,9 @@ public override TimeSpan IdleConnectionTimeout {
5653
set => idleConnectionTimeout = value;
5754
}
5855

56+
/// <inheritdoc/>
57+
public override HttpServerEngineContextEventLoopMecanism EventLoopMecanism => HttpServerEngineContextEventLoopMecanism.InlineAsyncronousGetContext;
58+
5959
/// <inheritdoc/>
6060
public override void AddListeningPrefix ( string prefix ) {
6161
prefixes.Add ( prefix );
@@ -88,6 +88,7 @@ public override void StartServer () {
8888
host.TimeoutManager.ClientWriteTimeout = idleConnectionTimeout;
8989
host.Handler = new CadenteHttpEngineHostHandler ( this );
9090
setupHostAction?.Invoke ( host );
91+
9192
host.Start ();
9293
hosts.Add ( host );
9394
}
@@ -110,37 +111,25 @@ public override void Dispose () {
110111
GC.SuppressFinalize ( this );
111112
}
112113

113-
internal void EnqueueContext ( CadenteHttpServerEngineContext context ) {
114-
if (_pendingContextRequests.TryDequeue ( out var tcs )) {
115-
tcs.SetResult ( context );
116-
}
117-
else {
118-
_readyContexts.Enqueue ( context );
119-
}
114+
/// <inheritdoc/>
115+
public override IAsyncResult BeginGetContext ( AsyncCallback? callback, object? state ) {
116+
throw new NotImplementedException ();
120117
}
121118

122119
/// <inheritdoc/>
123-
public override IAsyncResult BeginGetContext ( AsyncCallback? callback, object? state ) {
124-
if (_readyContexts.TryDequeue ( out var context )) {
125-
var tcs = new TaskCompletionSource<HttpServerEngineContext> ( state );
126-
tcs.SetResult ( context );
127-
callback?.Invoke ( tcs.Task );
128-
return tcs.Task;
129-
}
130-
else {
131-
var tcs = new TaskCompletionSource<HttpServerEngineContext> ( state );
132-
_pendingContextRequests.Enqueue ( tcs );
133-
if (callback != null) {
134-
tcs.Task.ContinueWith ( t => callback ( t ) );
135-
}
136-
return tcs.Task;
120+
public override HttpServerEngineContext EndGetContext ( IAsyncResult asyncResult ) {
121+
throw new NotImplementedException ();
122+
}
123+
124+
internal void EnqueueContext ( CadenteHttpServerEngineContext context ) {
125+
if (!_pendingContexts.Writer.TryWrite ( context )) {
126+
throw new InvalidOperationException ( "Failed to enqueue HTTP context." );
137127
}
138128
}
139129

140130
/// <inheritdoc/>
141-
public override HttpServerEngineContext EndGetContext ( IAsyncResult asyncResult ) {
142-
var task = (Task<HttpServerEngineContext>) asyncResult;
143-
return task.Result;
131+
public override async Task<HttpServerEngineContext> GetContextAsync ( CancellationToken cancellationToken = default ) {
132+
return await _pendingContexts.Reader.ReadAsync ( cancellationToken );
144133
}
145134
}
146135
}

cadente/Sisk.Cadente.CoreEngine/CadenteHttpServerEngineContext.cs

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@
88
// File name: CadenteHttpServerEngineContext.cs
99
// Repository: https://github.com/sisk-http/core
1010

11-
using System;
12-
using System.Collections.Generic;
13-
using System.Linq;
11+
using System.Net.WebSockets;
12+
using System.Security.Cryptography;
1413
using System.Text;
15-
using System.Threading.Tasks;
1614
using Sisk.Core.Http.Engine;
1715

1816
namespace Sisk.Cadente.CoreEngine {
@@ -30,7 +28,7 @@ public sealed class CadenteHttpServerEngineContext : HttpServerEngineContext {
3028
/// </summary>
3129
public Task ProcessingTask => _processingTcs.Task;
3230

33-
internal void CompleteProcessing () => _processingTcs.SetResult ( null );
31+
internal void CompleteProcessing () => _processingTcs.TrySetResult ( null );
3432

3533
/// <inheritdoc/>
3634
public CadenteHttpServerEngineContext ( CadenteHttpServerEngineRequest request, CadenteHttpServerEngineResponse response ) {
@@ -50,8 +48,49 @@ public CadenteHttpServerEngineContext ( CadenteHttpServerEngineRequest request,
5048

5149
/// <inheritdoc/>
5250
public override Task<HttpServerEngineWebSocket> AcceptWebSocketAsync ( string? subProtocol ) {
53-
// Sisk.Cadente does not support WebSockets yet.
54-
throw new NotSupportedException ( "Sisk.Cadente does not support WebSockets yet." );
51+
52+
string? wsKey = _request._context.Request.Headers.Get ( "Sec-WebSocket-Key" ).FirstOrDefault ()
53+
?? throw new InvalidOperationException ( "Missing 'Sec-WebSocket-Key' header in WebSocket upgrade request." );
54+
55+
byte [] wsAcceptToken = SHA1.HashData ( Encoding.UTF8.GetBytes ( $"{wsKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ) );
56+
57+
var underlyingResponse = _request._context.Response;
58+
59+
underlyingResponse.StatusCode = 101;
60+
underlyingResponse.StatusDescription = "Switching Protocols";
61+
62+
underlyingResponse.Headers.Set ( new HttpHeader ( "Connection", "Upgrade" ) );
63+
underlyingResponse.Headers.Set ( new HttpHeader ( "Upgrade", "websocket" ) );
64+
underlyingResponse.Headers.Set ( new HttpHeader ( "Sec-WebSocket-Accept", Convert.ToBase64String ( wsAcceptToken ) ) );
65+
underlyingResponse.Headers.Set ( new HttpHeader ( "Sec-WebSocket-Version", "13" ) );
66+
67+
if (subProtocol is { Length: > 0 }) {
68+
69+
string [] clientSubProtocols = _request._context.Request.Headers.Get ( "Sec-WebSocket-Protocol" )
70+
.SelectMany ( s => s.Split ( ",", StringSplitOptions.RemoveEmptyEntries ) )
71+
.Select ( s => s.Trim () )
72+
.ToArray ();
73+
74+
if (!clientSubProtocols.Contains ( subProtocol, StringComparer.Ordinal )) {
75+
76+
underlyingResponse.StatusCode = 426;
77+
underlyingResponse.StatusDescription = "Upgrade Required";
78+
79+
throw new InvalidOperationException ( $"The requested sub-protocol '{subProtocol}' is not supported by the client." );
80+
}
81+
82+
underlyingResponse.Headers.Set ( new HttpHeader ( "Sec-WebSocket-Protocol", subProtocol ) );
83+
}
84+
85+
Stream underlyingStream = underlyingResponse.GetResponseStream ( chunked: false );
86+
87+
var ws = WebSocket.CreateFromStream ( underlyingStream, new WebSocketCreationOptions () {
88+
IsServer = true,
89+
SubProtocol = subProtocol,
90+
KeepAliveInterval = TimeSpan.FromSeconds ( 20 )
91+
} );
92+
93+
return Task.FromResult ( HttpServerEngineWebSocket.CreateFromWebSocket ( ws ) );
5594
}
5695
}
5796
}

cadente/Sisk.Cadente.CoreEngine/CadenteHttpServerEngineRequest.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,10 @@
88
// File name: CadenteHttpServerEngineRequest.cs
99
// Repository: https://github.com/sisk-http/core
1010

11-
using System;
12-
using System.Collections.Generic;
1311
using System.Collections.Specialized;
14-
using System.Linq;
1512
using System.Net;
1613
using System.Net.Http.Headers;
17-
using System.Net.Mime;
1814
using System.Text;
19-
using System.Threading.Tasks;
2015
using Sisk.Core.Http;
2116
using Sisk.Core.Http.Engine;
2217

@@ -77,11 +72,7 @@ public override NameValueCollection QueryString {
7772
public override IPEndPoint RemoteEndPoint => _context.Client.ClientEndpoint;
7873

7974
/// <inheritdoc/>
80-
#if NET9_0_OR_GREATER
81-
public override Guid RequestTraceIdentifier => Guid.CreateVersion7 ();
82-
#else
83-
public override Guid RequestTraceIdentifier => Guid.NewGuid ();
84-
#endif
75+
public override Guid RequestTraceIdentifier { get; } = Guid.NewGuid ();
8576

8677
/// <inheritdoc/>
8778
public override NameValueCollection Headers {

cadente/Sisk.Cadente/HttpHeaderExtensions.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,47 @@ public static void Set ( this List<HttpHeader> headers, in HttpHeader header ) {
3737
}
3838
}
3939

40+
/// <summary>
41+
/// Retrieves all values for the specified header name from the list.
42+
/// This operation is thread‑safe.
43+
/// </summary>
44+
/// <param name="headers">The list of <see cref="HttpHeader"/> to query.</param>
45+
/// <param name="name">The name of the header to retrieve values for. May be <see langword="null"/>.</param>
46+
/// <returns>An array of header values that match the specified name.</returns>
47+
public static string [] Get ( this List<HttpHeader> headers, string? name ) {
48+
49+
List<string> results = new ();
50+
lock (((ICollection) headers).SyncRoot) {
51+
var span = CollectionsMarshal.AsSpan ( headers );
52+
for (int i = span.Length - 1; i >= 0; i--) {
53+
if (Ascii.EqualsIgnoreCase ( span [ i ].NameBytes.Span, name )) {
54+
results.Add ( span [ i ].Value );
55+
}
56+
}
57+
}
58+
59+
return results.ToArray ();
60+
}
61+
62+
/// <summary>
63+
/// Retrieves all values for the specified header name from the array.
64+
/// </summary>
65+
/// <param name="headers">The array of <see cref="HttpHeader"/> to query.</param>
66+
/// <param name="name">The name of the header to retrieve values for. May be <see langword="null"/>.</param>
67+
/// <returns>An array of header values that match the specified name.</returns>
68+
public static string [] Get ( this HttpHeader [] headers, string? name ) {
69+
70+
List<string> results = new ();
71+
var span = headers.AsSpan ();
72+
for (int i = span.Length - 1; i >= 0; i--) {
73+
if (Ascii.EqualsIgnoreCase ( span [ i ].NameBytes.Span, name )) {
74+
results.Add ( span [ i ].Value );
75+
}
76+
}
77+
78+
return results.ToArray ();
79+
}
80+
4081
/// <summary>
4182
/// Removes all <see cref="HttpHeader"/> with the given name from the list. Thread-safe.
4283
/// </summary>

cadente/Sisk.Cadente/Streams/HttpRequestStream.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public override void Flush () {
3838
}
3939

4040
public override int Read ( byte [] buffer, int offset, int count ) {
41-
if (read >= baseRequest.ContentLength) {
41+
if (baseRequest.ContentLength > 0 && read >= baseRequest.ContentLength) {
4242
return 0;
4343
}
4444

extensions/Sisk.BasicAuth/BasicAuthenticateRequestHandler.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
// File name: BasicAuthenticateRequestHandler.cs
88
// Repository: https://github.com/sisk-http/core
99

10-
using System.Text;
1110
using Sisk.Core.Http;
1211
using Sisk.Core.Routing;
1312

@@ -88,7 +87,7 @@ public BasicAuthenticateRequestHandler ( Func<BasicAuthenticationCredentials, Ht
8887
try {
8988
var auth = ParseAuth ( authorization );
9089
if (auth == null) {
91-
return DefaultMessagePage.CreateDefaultResponse ( HttpStatusInformation.BadRequest, "Invalid Authorization Header" );
90+
return DefaultMessagePage.Instance.CreateMessageHtml ( HttpStatusInformation.BadRequest, "Invalid Authorization Header" );
9291
}
9392
var res = OnValidating ( auth, context );
9493
return res;

src/Http/Engine/HttpListenerAbstractEngine.cs

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,9 @@
77
// File name: HttpListenerAbstractEngine.cs
88
// Repository: https://github.com/sisk-http/core
99

10-
using System;
11-
using System.Collections.Generic;
1210
using System.Collections.Specialized;
13-
using System.Linq;
1411
using System.Net;
15-
using System.Net.WebSockets;
1612
using System.Text;
17-
using System.Threading.Tasks;
1813

1914
namespace Sisk.Core.Http.Engine;
2015

@@ -45,6 +40,9 @@ public override TimeSpan IdleConnectionTimeout {
4540
set => _listener.TimeoutManager.IdleConnection = value;
4641
}
4742

43+
/// <inheritdoc/>
44+
public override HttpServerEngineContextEventLoopMecanism EventLoopMecanism => HttpServerEngineContextEventLoopMecanism.UnboundAsyncronousGetContext;
45+
4846
/// <inheritdoc/>
4947
public override void AddListeningPrefix ( string prefix ) {
5048
_listener.Prefixes.Add ( prefix );
@@ -81,6 +79,11 @@ public override void StopServer () {
8179
_listener.Stop ();
8280
}
8381

82+
/// <inheritdoc/>
83+
public override Task<HttpServerEngineContext> GetContextAsync ( CancellationToken cancellationToken = default ) {
84+
throw new NotImplementedException ();
85+
}
86+
8487
sealed class HttpListenerContextAbstraction ( HttpListenerContext context ) : HttpServerEngineContext {
8588

8689
HttpListenerContext _context = context;
@@ -93,7 +96,7 @@ sealed class HttpListenerContextAbstraction ( HttpListenerContext context ) : Ht
9396

9497
public override async Task<HttpServerEngineWebSocket> AcceptWebSocketAsync ( string? subProtocol ) {
9598
var ws = await _context.AcceptWebSocketAsync ( subProtocol ).ConfigureAwait ( false );
96-
return new HttpListenerContextWebSocketAbstraction ( ws.WebSocket );
99+
return HttpServerEngineWebSocket.CreateFromWebSocket ( ws.WebSocket );
97100
}
98101
}
99102

@@ -175,26 +178,4 @@ public void SetHeader ( string name, string value ) {
175178
}
176179
}
177180
}
178-
179-
sealed class HttpListenerContextWebSocketAbstraction ( WebSocket ws ) : HttpServerEngineWebSocket {
180-
readonly WebSocket _ws = ws;
181-
182-
public override WebSocketState State => _ws.State;
183-
184-
public override Task CloseAsync ( WebSocketCloseStatus closeStatus, string? reason, CancellationToken cancellation ) {
185-
return _ws.CloseAsync ( closeStatus, reason, cancellation );
186-
}
187-
188-
public override Task CloseOutputAsync ( WebSocketCloseStatus closeStatus, string? reason, CancellationToken cancellation ) {
189-
return _ws.CloseOutputAsync ( closeStatus, reason, cancellation );
190-
}
191-
192-
public override async ValueTask<ValueWebSocketReceiveResult> ReceiveAsync ( Memory<byte> buffer, CancellationToken cancellationToken ) {
193-
return await _ws.ReceiveAsync ( buffer, cancellationToken ).ConfigureAwait ( false );
194-
}
195-
196-
public override async ValueTask SendAsync ( ReadOnlyMemory<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken ) {
197-
await _ws.SendAsync ( buffer, messageType, endOfMessage, cancellationToken ).ConfigureAwait ( false );
198-
}
199-
}
200181
}

0 commit comments

Comments
 (0)